mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat: add user info edit verify (#4117)
This commit is contained in:
@@ -9,7 +9,12 @@ import { changeEmailMutation, changePasswordMutation } from '@affine/graphql';
|
|||||||
import { useMutation } from '@affine/workspace/affine/gql';
|
import { useMutation } from '@affine/workspace/affine/gql';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { type LoaderFunction, redirect, useParams } from 'react-router-dom';
|
import {
|
||||||
|
type LoaderFunction,
|
||||||
|
redirect,
|
||||||
|
useParams,
|
||||||
|
useSearchParams,
|
||||||
|
} from 'react-router-dom';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||||
@@ -27,6 +32,7 @@ const authTypeSchema = z.enum([
|
|||||||
export const AuthPage = (): ReactElement | null => {
|
export const AuthPage = (): ReactElement | null => {
|
||||||
const user = useCurrentUser();
|
const user = useCurrentUser();
|
||||||
const { authType } = useParams();
|
const { authType } = useParams();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const { trigger: changePassword } = useMutation({
|
const { trigger: changePassword } = useMutation({
|
||||||
mutation: changePasswordMutation,
|
mutation: changePasswordMutation,
|
||||||
});
|
});
|
||||||
@@ -39,22 +45,22 @@ export const AuthPage = (): ReactElement | null => {
|
|||||||
const onChangeEmail = useCallback(
|
const onChangeEmail = useCallback(
|
||||||
async (email: string) => {
|
async (email: string) => {
|
||||||
const res = await changeEmail({
|
const res = await changeEmail({
|
||||||
id: user.id,
|
token: searchParams.get('token') || '',
|
||||||
newEmail: email,
|
newEmail: email,
|
||||||
});
|
});
|
||||||
return !!res?.changeEmail;
|
return !!res?.changeEmail;
|
||||||
},
|
},
|
||||||
[changeEmail, user.id]
|
[changeEmail, searchParams]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSetPassword = useCallback(
|
const onSetPassword = useCallback(
|
||||||
(password: string) => {
|
(password: string) => {
|
||||||
changePassword({
|
changePassword({
|
||||||
id: user.id,
|
token: searchParams.get('token') || '',
|
||||||
newPassword: password,
|
newPassword: password,
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
},
|
},
|
||||||
[changePassword, user.id]
|
[changePassword, searchParams]
|
||||||
);
|
);
|
||||||
const onOpenAffine = useCallback(() => {
|
const onOpenAffine = useCallback(() => {
|
||||||
jumpToIndex(RouteLogic.REPLACE);
|
jumpToIndex(RouteLogic.REPLACE);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"@aws-sdk/client-s3": "^3.400.0",
|
"@aws-sdk/client-s3": "^3.400.0",
|
||||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.17.0",
|
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.17.0",
|
||||||
"@google-cloud/opentelemetry-cloud-trace-exporter": "^2.1.0",
|
"@google-cloud/opentelemetry-cloud-trace-exporter": "^2.1.0",
|
||||||
|
"@keyv/redis": "^2.7.0",
|
||||||
"@nestjs/apollo": "^12.0.7",
|
"@nestjs/apollo": "^12.0.7",
|
||||||
"@nestjs/common": "^10.2.4",
|
"@nestjs/common": "^10.2.4",
|
||||||
"@nestjs/core": "^10.2.4",
|
"@nestjs/core": "^10.2.4",
|
||||||
@@ -57,6 +58,7 @@
|
|||||||
"graphql-type-json": "^0.3.2",
|
"graphql-type-json": "^0.3.2",
|
||||||
"graphql-upload": "^16.0.2",
|
"graphql-upload": "^16.0.2",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
|
"keyv": "^4.5.3",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"nestjs-throttler-storage-redis": "^0.3.3",
|
"nestjs-throttler-storage-redis": "^0.3.3",
|
||||||
"next-auth": "4.22.5",
|
"next-auth": "4.22.5",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ConfigModule } from './config';
|
|||||||
import { MetricsModule } from './metrics';
|
import { MetricsModule } from './metrics';
|
||||||
import { BusinessModules, Providers } from './modules';
|
import { BusinessModules, Providers } from './modules';
|
||||||
import { PrismaModule } from './prisma';
|
import { PrismaModule } from './prisma';
|
||||||
|
import { SessionModule } from './session';
|
||||||
import { StorageModule } from './storage';
|
import { StorageModule } from './storage';
|
||||||
import { RateLimiterModule } from './throttler';
|
import { RateLimiterModule } from './throttler';
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ import { RateLimiterModule } from './throttler';
|
|||||||
ConfigModule.forRoot(),
|
ConfigModule.forRoot(),
|
||||||
StorageModule.forRoot(),
|
StorageModule.forRoot(),
|
||||||
MetricsModule,
|
MetricsModule,
|
||||||
|
SessionModule,
|
||||||
RateLimiterModule,
|
RateLimiterModule,
|
||||||
...BusinessModules,
|
...BusinessModules,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -231,6 +231,15 @@ export interface AFFiNEConfig {
|
|||||||
port: number;
|
port: number;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
/**
|
||||||
|
* redis database index
|
||||||
|
*
|
||||||
|
* Rate Limiter scope: database + 1
|
||||||
|
*
|
||||||
|
* Session scope: database + 2
|
||||||
|
*
|
||||||
|
* @default 0
|
||||||
|
*/
|
||||||
database: number;
|
database: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { PrismaAdapter } from '@auth/prisma-adapter';
|
|||||||
import { BadRequestException, FactoryProvider, Logger } from '@nestjs/common';
|
import { BadRequestException, FactoryProvider, Logger } from '@nestjs/common';
|
||||||
import { verify } from '@node-rs/argon2';
|
import { verify } from '@node-rs/argon2';
|
||||||
import { Algorithm, sign, verify as jwtVerify } from '@node-rs/jsonwebtoken';
|
import { Algorithm, sign, verify as jwtVerify } from '@node-rs/jsonwebtoken';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
import { NextAuthOptions } from 'next-auth';
|
import { NextAuthOptions } from 'next-auth';
|
||||||
import Credentials from 'next-auth/providers/credentials';
|
import Credentials from 'next-auth/providers/credentials';
|
||||||
import Email, {
|
import Email, {
|
||||||
@@ -14,6 +15,7 @@ import Google from 'next-auth/providers/google';
|
|||||||
|
|
||||||
import { Config } from '../../config';
|
import { Config } from '../../config';
|
||||||
import { PrismaService } from '../../prisma';
|
import { PrismaService } from '../../prisma';
|
||||||
|
import { SessionService } from '../../session';
|
||||||
import { NewFeaturesKind } from '../users/types';
|
import { NewFeaturesKind } from '../users/types';
|
||||||
import { isStaff } from '../users/utils';
|
import { isStaff } from '../users/utils';
|
||||||
import { MailService } from './mailer';
|
import { MailService } from './mailer';
|
||||||
@@ -23,7 +25,12 @@ export const NextAuthOptionsProvide = Symbol('NextAuthOptions');
|
|||||||
|
|
||||||
export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
|
export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
|
||||||
provide: NextAuthOptionsProvide,
|
provide: NextAuthOptionsProvide,
|
||||||
useFactory(config: Config, prisma: PrismaService, mailer: MailService) {
|
useFactory(
|
||||||
|
config: Config,
|
||||||
|
prisma: PrismaService,
|
||||||
|
mailer: MailService,
|
||||||
|
session: SessionService
|
||||||
|
) {
|
||||||
const logger = new Logger('NextAuth');
|
const logger = new Logger('NextAuth');
|
||||||
const prismaAdapter = PrismaAdapter(prisma);
|
const prismaAdapter = PrismaAdapter(prisma);
|
||||||
// createUser exists in the adapter
|
// createUser exists in the adapter
|
||||||
@@ -72,15 +79,31 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
|
|||||||
from: config.auth.email.sender,
|
from: config.auth.email.sender,
|
||||||
async sendVerificationRequest(params: SendVerificationRequestParams) {
|
async sendVerificationRequest(params: SendVerificationRequestParams) {
|
||||||
const { identifier, url, provider } = params;
|
const { identifier, url, provider } = params;
|
||||||
const { searchParams } = new URL(url);
|
const urlWithToken = new URL(url);
|
||||||
const callbackUrl = searchParams.get('callbackUrl') || '';
|
const callbackUrl =
|
||||||
|
urlWithToken.searchParams.get('callbackUrl') || '';
|
||||||
if (!callbackUrl) {
|
if (!callbackUrl) {
|
||||||
throw new Error('callbackUrl is not set');
|
throw new Error('callbackUrl is not set');
|
||||||
|
} else {
|
||||||
|
const newCallbackUrl = new URL(callbackUrl, config.origin);
|
||||||
|
|
||||||
|
const token = nanoid();
|
||||||
|
await session.set(token, identifier);
|
||||||
|
newCallbackUrl.searchParams.set('token', token);
|
||||||
|
|
||||||
|
urlWithToken.searchParams.set(
|
||||||
|
'callbackUrl',
|
||||||
|
newCallbackUrl.toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const result = await mailer.sendSignInEmail(url, {
|
|
||||||
to: identifier,
|
const result = await mailer.sendSignInEmail(
|
||||||
from: provider.from,
|
urlWithToken.toString(),
|
||||||
});
|
{
|
||||||
|
to: identifier,
|
||||||
|
from: provider.from,
|
||||||
|
}
|
||||||
|
);
|
||||||
logger.log(
|
logger.log(
|
||||||
`send verification email success: ${result.accepted.join(', ')}`
|
`send verification email success: ${result.accepted.join(', ')}`
|
||||||
);
|
);
|
||||||
@@ -277,5 +300,5 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
|
|||||||
};
|
};
|
||||||
return nextAuthOptions;
|
return nextAuthOptions;
|
||||||
},
|
},
|
||||||
inject: [Config, PrismaService, MailService],
|
inject: [Config, PrismaService, MailService, SessionService],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -127,7 +127,6 @@ export class NextAuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (redirect?.endsWith('api/auth/error?error=AccessDenied')) {
|
if (redirect?.endsWith('api/auth/error?error=AccessDenied')) {
|
||||||
this.logger.debug(req.headers);
|
|
||||||
if (!req.headers?.referer) {
|
if (!req.headers?.referer) {
|
||||||
res.redirect('https://community.affine.pro/c/insider-general/');
|
res.redirect('https://community.affine.pro/c/insider-general/');
|
||||||
} else {
|
} else {
|
||||||
@@ -145,7 +144,6 @@ export class NextAuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
this.logger.debug(providerId, action, req.headers);
|
|
||||||
if (providerId === 'credentials') {
|
if (providerId === 'credentials') {
|
||||||
res.send(JSON.stringify({ ok: true, url: redirect }));
|
res.send(JSON.stringify({ ok: true, url: redirect }));
|
||||||
} else if (
|
} else if (
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { ForbiddenException, UseGuards } from '@nestjs/common';
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
ForbiddenException,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
Args,
|
Args,
|
||||||
Context,
|
Context,
|
||||||
@@ -10,11 +14,13 @@ import {
|
|||||||
Resolver,
|
Resolver,
|
||||||
} from '@nestjs/graphql';
|
} from '@nestjs/graphql';
|
||||||
import type { Request } from 'express';
|
import type { Request } from 'express';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
import { Config } from '../../config';
|
import { Config } from '../../config';
|
||||||
|
import { SessionService } from '../../session';
|
||||||
import { CloudThrottlerGuard, Throttle } from '../../throttler';
|
import { CloudThrottlerGuard, Throttle } from '../../throttler';
|
||||||
import { UserType } from '../users/resolver';
|
import { UserType } from '../users/resolver';
|
||||||
import { CurrentUser } from './guard';
|
import { Auth, CurrentUser } from './guard';
|
||||||
import { AuthService } from './service';
|
import { AuthService } from './service';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
@@ -37,14 +43,15 @@ export class TokenType {
|
|||||||
export class AuthResolver {
|
export class AuthResolver {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly config: Config,
|
private readonly config: Config,
|
||||||
private auth: AuthService
|
private auth: AuthService,
|
||||||
|
private readonly session: SessionService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Throttle(20, 60)
|
@Throttle(20, 60)
|
||||||
@ResolveField(() => TokenType)
|
@ResolveField(() => TokenType)
|
||||||
token(@CurrentUser() currentUser: UserType, @Parent() user: UserType) {
|
token(@CurrentUser() currentUser: UserType, @Parent() user: UserType) {
|
||||||
if (user.id !== currentUser.id) {
|
if (user.id !== currentUser.id) {
|
||||||
throw new ForbiddenException();
|
throw new BadRequestException('Invalid user');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -80,58 +87,93 @@ export class AuthResolver {
|
|||||||
|
|
||||||
@Throttle(5, 60)
|
@Throttle(5, 60)
|
||||||
@Mutation(() => UserType)
|
@Mutation(() => UserType)
|
||||||
|
@Auth()
|
||||||
async changePassword(
|
async changePassword(
|
||||||
@Context() ctx: { req: Request },
|
@CurrentUser() user: UserType,
|
||||||
@Args('id') id: string,
|
@Args('token') token: string,
|
||||||
@Args('newPassword') newPassword: string
|
@Args('newPassword') newPassword: string
|
||||||
) {
|
) {
|
||||||
const user = await this.auth.changePassword(id, newPassword);
|
const id = await this.session.get(token);
|
||||||
ctx.req.user = user;
|
if (!id || id !== user.id) {
|
||||||
|
throw new ForbiddenException('Invalid token');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.auth.changePassword(id, newPassword);
|
||||||
|
await this.session.delete(token);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throttle(5, 60)
|
@Throttle(5, 60)
|
||||||
@Mutation(() => UserType)
|
@Mutation(() => UserType)
|
||||||
|
@Auth()
|
||||||
async changeEmail(
|
async changeEmail(
|
||||||
@Context() ctx: { req: Request },
|
@CurrentUser() user: UserType,
|
||||||
@Args('id') id: string,
|
@Args('token') token: string,
|
||||||
@Args('email') email: string
|
@Args('email') email: string
|
||||||
) {
|
) {
|
||||||
const user = await this.auth.changeEmail(id, email);
|
const id = await this.session.get(token);
|
||||||
ctx.req.user = user;
|
if (!id || id !== user.id) {
|
||||||
|
throw new ForbiddenException('Invalid token');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.auth.changeEmail(id, email);
|
||||||
|
await this.session.delete(token);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throttle(5, 60)
|
@Throttle(5, 60)
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
|
@Auth()
|
||||||
async sendChangePasswordEmail(
|
async sendChangePasswordEmail(
|
||||||
|
@CurrentUser() user: UserType,
|
||||||
@Args('email') email: string,
|
@Args('email') email: string,
|
||||||
@Args('callbackUrl') callbackUrl: string
|
@Args('callbackUrl') callbackUrl: string
|
||||||
) {
|
) {
|
||||||
const url = `${this.config.baseUrl}${callbackUrl}`;
|
const token = nanoid();
|
||||||
const res = await this.auth.sendChangePasswordEmail(email, url);
|
await this.session.set(token, user.id);
|
||||||
|
|
||||||
|
const url = new URL(callbackUrl, this.config.baseUrl);
|
||||||
|
url.searchParams.set('token', token);
|
||||||
|
|
||||||
|
const res = await this.auth.sendChangePasswordEmail(email, url.toString());
|
||||||
return !res.rejected.length;
|
return !res.rejected.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throttle(5, 60)
|
@Throttle(5, 60)
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
|
@Auth()
|
||||||
async sendSetPasswordEmail(
|
async sendSetPasswordEmail(
|
||||||
|
@CurrentUser() user: UserType,
|
||||||
@Args('email') email: string,
|
@Args('email') email: string,
|
||||||
@Args('callbackUrl') callbackUrl: string
|
@Args('callbackUrl') callbackUrl: string
|
||||||
) {
|
) {
|
||||||
const url = `${this.config.baseUrl}${callbackUrl}`;
|
const token = nanoid();
|
||||||
const res = await this.auth.sendSetPasswordEmail(email, url);
|
await this.session.set(token, user.id);
|
||||||
|
|
||||||
|
const url = new URL(callbackUrl, this.config.baseUrl);
|
||||||
|
url.searchParams.set('token', token);
|
||||||
|
|
||||||
|
const res = await this.auth.sendSetPasswordEmail(email, url.toString());
|
||||||
return !res.rejected.length;
|
return !res.rejected.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throttle(5, 60)
|
@Throttle(5, 60)
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
|
@Auth()
|
||||||
async sendChangeEmail(
|
async sendChangeEmail(
|
||||||
|
@CurrentUser() user: UserType,
|
||||||
@Args('email') email: string,
|
@Args('email') email: string,
|
||||||
@Args('callbackUrl') callbackUrl: string
|
@Args('callbackUrl') callbackUrl: string
|
||||||
) {
|
) {
|
||||||
const url = `${this.config.baseUrl}${callbackUrl}`;
|
const token = nanoid();
|
||||||
const res = await this.auth.sendChangeEmail(email, url);
|
await this.session.set(token, user.id);
|
||||||
|
|
||||||
|
const url = new URL(callbackUrl, this.config.baseUrl);
|
||||||
|
url.searchParams.set('token', token);
|
||||||
|
|
||||||
|
const res = await this.auth.sendChangeEmail(email, url.toString());
|
||||||
return !res.rejected.length;
|
return !res.rejected.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export class UserResolver {
|
|||||||
name: 'currentUser',
|
name: 'currentUser',
|
||||||
description: 'Get current user',
|
description: 'Get current user',
|
||||||
})
|
})
|
||||||
async currentUser(@CurrentUser() user: User) {
|
async currentUser(@CurrentUser() user: UserType) {
|
||||||
const storedUser = await this.prisma.user.findUnique({
|
const storedUser = await this.prisma.user.findUnique({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -186,8 +186,8 @@ type Mutation {
|
|||||||
addToNewFeaturesWaitingList(type: NewFeaturesKind!, email: String!): AddToNewFeaturesWaitingList!
|
addToNewFeaturesWaitingList(type: NewFeaturesKind!, email: String!): AddToNewFeaturesWaitingList!
|
||||||
signUp(name: String!, email: String!, password: String!): UserType!
|
signUp(name: String!, email: String!, password: String!): UserType!
|
||||||
signIn(email: String!, password: String!): UserType!
|
signIn(email: String!, password: String!): UserType!
|
||||||
changePassword(id: String!, newPassword: String!): UserType!
|
changePassword(token: String!, newPassword: String!): UserType!
|
||||||
changeEmail(id: String!, email: String!): UserType!
|
changeEmail(token: String!, email: String!): UserType!
|
||||||
sendChangePasswordEmail(email: String!, callbackUrl: String!): Boolean!
|
sendChangePasswordEmail(email: String!, callbackUrl: String!): Boolean!
|
||||||
sendSetPasswordEmail(email: String!, callbackUrl: String!): Boolean!
|
sendSetPasswordEmail(email: String!, callbackUrl: String!): Boolean!
|
||||||
sendChangeEmail(email: String!, callbackUrl: String!): Boolean!
|
sendChangeEmail(email: String!, callbackUrl: String!): Boolean!
|
||||||
|
|||||||
60
apps/server/src/session.ts
Normal file
60
apps/server/src/session.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import KeyvRedis from '@keyv/redis';
|
||||||
|
import { Global, Injectable, Module } from '@nestjs/common';
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
import Keyv from 'keyv';
|
||||||
|
|
||||||
|
import { Config } from './config';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SessionService {
|
||||||
|
private readonly cache: Keyv;
|
||||||
|
private readonly prefix = 'session:';
|
||||||
|
private readonly sessionTtl = 30 * 60 * 1000; // 30 min
|
||||||
|
|
||||||
|
constructor(protected readonly config: Config) {
|
||||||
|
if (config.redis.enabled) {
|
||||||
|
this.cache = new Keyv({
|
||||||
|
store: new KeyvRedis(
|
||||||
|
new Redis(config.redis.port, config.redis.host, {
|
||||||
|
username: config.redis.username,
|
||||||
|
password: config.redis.password,
|
||||||
|
db: config.redis.database + 2,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.cache = new Keyv();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get session
|
||||||
|
* @param key session key
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async get(key: string) {
|
||||||
|
return this.cache.get(this.prefix + key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set session
|
||||||
|
* @param key session key
|
||||||
|
* @param value session value
|
||||||
|
* @param sessionTtl session ttl (ms), default 30 min
|
||||||
|
* @returns return true if success
|
||||||
|
*/
|
||||||
|
async set(key: string, value?: any, sessionTtl = this.sessionTtl) {
|
||||||
|
return this.cache.set(this.prefix + key, value, sessionTtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(key: string) {
|
||||||
|
return this.cache.delete(this.prefix + key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [SessionService],
|
||||||
|
exports: [SessionService],
|
||||||
|
})
|
||||||
|
export class SessionModule {}
|
||||||
42
apps/server/src/tests/session.spec.ts
Normal file
42
apps/server/src/tests/session.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/// <reference types="../global.d.ts" />
|
||||||
|
import { equal } from 'node:assert';
|
||||||
|
import { afterEach, beforeEach, test } from 'node:test';
|
||||||
|
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
import { ConfigModule } from '../config';
|
||||||
|
import { SessionModule, SessionService } from '../session';
|
||||||
|
|
||||||
|
let session: SessionService;
|
||||||
|
let module: TestingModule;
|
||||||
|
|
||||||
|
// cleanup database before each test
|
||||||
|
beforeEach(async () => {
|
||||||
|
const client = new PrismaClient();
|
||||||
|
await client.$connect();
|
||||||
|
await client.user.deleteMany({});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
module = await Test.createTestingModule({
|
||||||
|
imports: [ConfigModule.forRoot(), SessionModule],
|
||||||
|
}).compile();
|
||||||
|
session = module.get(SessionService);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await module.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be able to set session', async () => {
|
||||||
|
await session.set('test', 'value');
|
||||||
|
equal(await session.get('test'), 'value');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be expired by ttl', async () => {
|
||||||
|
await session.set('test', 'value', 100);
|
||||||
|
equal(await session.get('test'), 'value');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
equal(await session.get('test'), undefined);
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
mutation changeEmail($id: String!, $newEmail: String!) {
|
mutation changeEmail($token: String!, $newEmail: String!) {
|
||||||
changeEmail(id: $id, email: $newEmail) {
|
changeEmail(token: $token, email: $newEmail) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
avatarUrl
|
avatarUrl
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
mutation changePassword($id: String!, $newPassword: String!) {
|
mutation changePassword($token: String!, $newPassword: String!) {
|
||||||
changePassword(id: $id, newPassword: $newPassword) {
|
changePassword(token: $token, newPassword: $newPassword) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
avatarUrl
|
avatarUrl
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ export const changeEmailMutation = {
|
|||||||
definitionName: 'changeEmail',
|
definitionName: 'changeEmail',
|
||||||
containsFile: false,
|
containsFile: false,
|
||||||
query: `
|
query: `
|
||||||
mutation changeEmail($id: String!, $newEmail: String!) {
|
mutation changeEmail($token: String!, $newEmail: String!) {
|
||||||
changeEmail(id: $id, email: $newEmail) {
|
changeEmail(token: $token, email: $newEmail) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
avatarUrl
|
avatarUrl
|
||||||
@@ -88,8 +88,8 @@ export const changePasswordMutation = {
|
|||||||
definitionName: 'changePassword',
|
definitionName: 'changePassword',
|
||||||
containsFile: false,
|
containsFile: false,
|
||||||
query: `
|
query: `
|
||||||
mutation changePassword($id: String!, $newPassword: String!) {
|
mutation changePassword($token: String!, $newPassword: String!) {
|
||||||
changePassword(id: $id, newPassword: $newPassword) {
|
changePassword(token: $token, newPassword: $newPassword) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
avatarUrl
|
avatarUrl
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export type AllBlobSizesQuery = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ChangeEmailMutationVariables = Exact<{
|
export type ChangeEmailMutationVariables = Exact<{
|
||||||
id: Scalars['String']['input'];
|
token: Scalars['String']['input'];
|
||||||
newEmail: Scalars['String']['input'];
|
newEmail: Scalars['String']['input'];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ export type ChangeEmailMutation = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ChangePasswordMutationVariables = Exact<{
|
export type ChangePasswordMutationVariables = Exact<{
|
||||||
id: Scalars['String']['input'];
|
token: Scalars['String']['input'];
|
||||||
newPassword: Scalars['String']['input'];
|
newPassword: Scalars['String']['input'];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|||||||
11
yarn.lock
11
yarn.lock
@@ -661,6 +661,7 @@ __metadata:
|
|||||||
"@aws-sdk/client-s3": ^3.400.0
|
"@aws-sdk/client-s3": ^3.400.0
|
||||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": ^0.17.0
|
"@google-cloud/opentelemetry-cloud-monitoring-exporter": ^0.17.0
|
||||||
"@google-cloud/opentelemetry-cloud-trace-exporter": ^2.1.0
|
"@google-cloud/opentelemetry-cloud-trace-exporter": ^2.1.0
|
||||||
|
"@keyv/redis": ^2.7.0
|
||||||
"@napi-rs/image": ^1.6.1
|
"@napi-rs/image": ^1.6.1
|
||||||
"@nestjs/apollo": ^12.0.7
|
"@nestjs/apollo": ^12.0.7
|
||||||
"@nestjs/common": ^10.2.4
|
"@nestjs/common": ^10.2.4
|
||||||
@@ -708,6 +709,7 @@ __metadata:
|
|||||||
graphql-type-json: ^0.3.2
|
graphql-type-json: ^0.3.2
|
||||||
graphql-upload: ^16.0.2
|
graphql-upload: ^16.0.2
|
||||||
ioredis: ^5.3.2
|
ioredis: ^5.3.2
|
||||||
|
keyv: ^4.5.3
|
||||||
lodash-es: ^4.17.21
|
lodash-es: ^4.17.21
|
||||||
nestjs-throttler-storage-redis: ^0.3.3
|
nestjs-throttler-storage-redis: ^0.3.3
|
||||||
next-auth: 4.22.5
|
next-auth: 4.22.5
|
||||||
@@ -6386,6 +6388,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@keyv/redis@npm:^2.7.0":
|
||||||
|
version: 2.7.0
|
||||||
|
resolution: "@keyv/redis@npm:2.7.0"
|
||||||
|
dependencies:
|
||||||
|
ioredis: ^5.3.2
|
||||||
|
checksum: 2bf16d99f54fa5177c375eb170f46076715e6a17fd65840d10638e441af8a4dd065927a18b45abb9531746ddab54f865884347c80f7e188c981a40f8245269ab
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@kwsites/file-exists@npm:^1.1.1":
|
"@kwsites/file-exists@npm:^1.1.1":
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
resolution: "@kwsites/file-exists@npm:1.1.1"
|
resolution: "@kwsites/file-exists@npm:1.1.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user