feat: improve authentication and cookie management
- Updated Docker and Turbo configuration to include more environment variables - Modified API configuration to support dynamic cookie and CORS settings - Enhanced user authentication flow with optional device ID and automatic generation - Refactored login, register, and logout resolvers to handle device management - Updated GraphQL schema to make device ID optional - Improved web application logout and authentication handling - Simplified client-side GraphQL mutations for login and registration
This commit is contained in:
parent
d29d116214
commit
f9c6230101
15 changed files with 190 additions and 148 deletions
|
@ -1,4 +1,8 @@
|
|||
.env
|
||||
node_modules
|
||||
**/dist
|
||||
dist
|
||||
.env
|
||||
.env.local
|
||||
.git
|
||||
.gitignore
|
||||
node_modules
|
||||
.turbo
|
||||
|
|
|
@ -11,6 +11,7 @@ const schema = z
|
|||
API_HOST: z.string(),
|
||||
API_PORT: z.coerce.number(),
|
||||
COOKIE_SECRET: z.string(),
|
||||
DATABASE_URL: z.string(),
|
||||
MINIO_ENDPOINT: z.string(),
|
||||
MINIO_PORT: z.coerce.number(),
|
||||
MINIO_ACCESS_KEY: z.string(),
|
||||
|
@ -34,6 +35,7 @@ const schema = z
|
|||
allowedOrigins: env.ALLOWED_ORIGINS.split(','),
|
||||
port: env.API_PORT,
|
||||
host: env.API_HOST,
|
||||
databaseUrl: env.DATABASE_URL,
|
||||
},
|
||||
minio: {
|
||||
endPoint: env.MINIO_ENDPOINT,
|
||||
|
@ -45,8 +47,8 @@ const schema = z
|
|||
region: env.MINIO_REGION,
|
||||
},
|
||||
token: {
|
||||
accessTokenExpiresIn: env.TOKEN_ACCESS_EXPIRES_IN,
|
||||
refreshTokenExpiresIn: env.TOKEN_REFRESH_EXPIRES_IN,
|
||||
accessTokenExpiresIn: env.TOKEN_ACCESS_EXPIRES_IN * 1000,
|
||||
refreshTokenExpiresIn: env.TOKEN_REFRESH_EXPIRES_IN * 1000,
|
||||
secret: env.TOKEN_SECRET,
|
||||
},
|
||||
memc: {
|
||||
|
|
|
@ -140,21 +140,21 @@ export type MutationregisterArgs = {
|
|||
email: Scalars["String"];
|
||||
username: Scalars["String"];
|
||||
password: Scalars["String"];
|
||||
deviceId: Scalars["String"];
|
||||
deviceId?: InputMaybe<Scalars["String"]>;
|
||||
};
|
||||
|
||||
export type MutationloginArgs = {
|
||||
email: Scalars["String"];
|
||||
password: Scalars["String"];
|
||||
deviceId: Scalars["String"];
|
||||
deviceId?: InputMaybe<Scalars["String"]>;
|
||||
};
|
||||
|
||||
export type MutationlogoutArgs = {
|
||||
deviceId: Scalars["String"];
|
||||
deviceId?: InputMaybe<Scalars["String"]>;
|
||||
};
|
||||
|
||||
export type MutationrefreshTokenArgs = {
|
||||
deviceId: Scalars["String"];
|
||||
deviceId?: InputMaybe<Scalars["String"]>;
|
||||
};
|
||||
|
||||
export type MutationcreateRoomArgs = {
|
||||
|
@ -495,28 +495,25 @@ export type MutationResolvers<
|
|||
ResolversTypes["AuthPayload"],
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<
|
||||
MutationregisterArgs,
|
||||
"email" | "username" | "password" | "deviceId"
|
||||
>
|
||||
RequireFields<MutationregisterArgs, "email" | "username" | "password">
|
||||
>;
|
||||
login?: Resolver<
|
||||
ResolversTypes["AuthPayload"],
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<MutationloginArgs, "email" | "password" | "deviceId">
|
||||
RequireFields<MutationloginArgs, "email" | "password">
|
||||
>;
|
||||
logout?: Resolver<
|
||||
ResolversTypes["Boolean"],
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<MutationlogoutArgs, "deviceId">
|
||||
Partial<MutationlogoutArgs>
|
||||
>;
|
||||
refreshToken?: Resolver<
|
||||
ResolversTypes["AuthPayload"],
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<MutationrefreshTokenArgs, "deviceId">
|
||||
Partial<MutationrefreshTokenArgs>
|
||||
>;
|
||||
logoutAllDevices?: Resolver<
|
||||
ResolversTypes["Boolean"],
|
||||
|
|
|
@ -56,8 +56,9 @@ app.register(fastifyCookie, {
|
|||
secret: cookieConfig.secret,
|
||||
parseOptions: {
|
||||
secure: config.isProduction,
|
||||
httpOnly: config.isProduction,
|
||||
sameSite: 'lax',
|
||||
httpOnly: true,
|
||||
sameSite: config.isProduction ? 'none' : 'lax',
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 30, // 30 days
|
||||
signed: true,
|
||||
},
|
||||
|
@ -69,7 +70,7 @@ app.register(fastifyCors, {
|
|||
return callback(new Error('Not allowed'), false);
|
||||
},
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
app.register(mercurius, {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { GraphQLError } from 'graphql';
|
||||
import { IResolvers } from 'mercurius';
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { MutationResolvers, QueryResolvers } from '../generated/graphql';
|
||||
import { withAuth, hashPassword, verifyPassword } from '../utils';
|
||||
|
||||
|
@ -66,9 +66,10 @@ export const userResolvers: IResolvers = {
|
|||
Mutation: {
|
||||
register: async (
|
||||
_,
|
||||
{ email, username, password, deviceId },
|
||||
{ prisma, token, reply }
|
||||
{ email, username, password, deviceId: reqDeviceId },
|
||||
{ prisma, token, reply, req }
|
||||
) => {
|
||||
const deviceId = req.cookies?.deviceId ?? reqDeviceId ?? uuidv4();
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ email }, { username }],
|
||||
|
@ -101,6 +102,7 @@ export const userResolvers: IResolvers = {
|
|||
}
|
||||
);
|
||||
reply.setCookie('refreshToken', refreshToken);
|
||||
reply.setCookie('deviceId', deviceId);
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
|
@ -109,9 +111,10 @@ export const userResolvers: IResolvers = {
|
|||
},
|
||||
login: async (
|
||||
_,
|
||||
{ email, password, deviceId },
|
||||
{ prisma, token, reply }
|
||||
{ email, password, deviceId: reqDeviceId },
|
||||
{ prisma, token, reply, req }
|
||||
) => {
|
||||
const deviceId = req.cookies?.deviceId ?? reqDeviceId ?? uuidv4();
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
@ -128,19 +131,27 @@ export const userResolvers: IResolvers = {
|
|||
role: 'user',
|
||||
deviceId,
|
||||
});
|
||||
|
||||
reply.setCookie('refreshToken', refreshToken);
|
||||
reply.setCookie('deviceId', deviceId);
|
||||
return {
|
||||
user,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
};
|
||||
},
|
||||
logout: async (_, { deviceId }, { token, jwt, reply }) => {
|
||||
jwt &&
|
||||
(await token.revokeTokensByDevice({
|
||||
logout: async (
|
||||
_,
|
||||
{ deviceId: reqDeviceId },
|
||||
{ token, jwt, reply, req }
|
||||
) => {
|
||||
const deviceId = req.cookies?.deviceId ?? reqDeviceId ?? uuidv4();
|
||||
if (jwt) {
|
||||
await token.revokeTokensByDevice({
|
||||
userId: jwt.sub,
|
||||
deviceId,
|
||||
}));
|
||||
});
|
||||
}
|
||||
reply.clearCookie('refreshToken');
|
||||
return true;
|
||||
},
|
||||
|
@ -152,13 +163,15 @@ export const userResolvers: IResolvers = {
|
|||
}
|
||||
),
|
||||
refreshToken: withAuth<MutationResolvers['refreshToken']>(
|
||||
async (_, { deviceId }, { token, jwt, reply }) => {
|
||||
async (_, { deviceId: reqDeviceId }, { token, jwt, reply }) => {
|
||||
const deviceId = reply.cookies?.['deviceId'] ?? reqDeviceId ?? uuidv4();
|
||||
const { accessToken, refreshToken } = await token.rotateRefreshToken({
|
||||
userId: jwt.sub,
|
||||
oldToken: jwt.jti,
|
||||
deviceId,
|
||||
});
|
||||
reply.setCookie('refreshToken', refreshToken);
|
||||
reply.setCookie('deviceId', deviceId);
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
|
|
|
@ -70,11 +70,11 @@ export default gql`
|
|||
email: String!
|
||||
username: String!
|
||||
password: String!
|
||||
deviceId: String!
|
||||
deviceId: String
|
||||
): AuthPayload!
|
||||
login(email: String!, password: String!, deviceId: String!): AuthPayload!
|
||||
logout(deviceId: String!): Boolean!
|
||||
refreshToken(deviceId: String!): AuthPayload!
|
||||
login(email: String!, password: String!, deviceId: String): AuthPayload!
|
||||
logout(deviceId: String): Boolean!
|
||||
refreshToken(deviceId: String): AuthPayload!
|
||||
logoutAllDevices: Boolean!
|
||||
createRoom(name: String!, description: String, isPrivate: Boolean): Room!
|
||||
joinRoom(roomId: ID!): Room!
|
||||
|
|
|
@ -38,12 +38,11 @@ export class MemcService {
|
|||
* Stores a value in Memcached
|
||||
* @param key - The key to store the value under
|
||||
* @param value - The value to store
|
||||
* @param ttl - Optional time-to-live in seconds (overrides default TTL)
|
||||
* @param ttl - Optional time-to-live in milliseconds (overrides default TTL)
|
||||
*/
|
||||
public async set(key: string, value: string, ttl?: number): Promise<boolean> {
|
||||
try {
|
||||
const ttlMilliseconds = ttl ? ttl * 1000 : undefined;
|
||||
return await this.keyv.set(key, value, ttlMilliseconds);
|
||||
return await this.keyv.set(key, value, ttl);
|
||||
} catch (error) {
|
||||
console.error('Error setting value in Memcached:', error);
|
||||
return false;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue