feat: add user info edit verify (#4117)

This commit is contained in:
DarkSky
2023-09-02 00:59:33 +08:00
committed by GitHub
parent db3a6efaf3
commit 3c4f45bcb6
16 changed files with 241 additions and 46 deletions
@@ -4,6 +4,7 @@ import { PrismaAdapter } from '@auth/prisma-adapter';
import { BadRequestException, FactoryProvider, Logger } from '@nestjs/common';
import { verify } from '@node-rs/argon2';
import { Algorithm, sign, verify as jwtVerify } from '@node-rs/jsonwebtoken';
import { nanoid } from 'nanoid';
import { NextAuthOptions } from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import Email, {
@@ -14,6 +15,7 @@ import Google from 'next-auth/providers/google';
import { Config } from '../../config';
import { PrismaService } from '../../prisma';
import { SessionService } from '../../session';
import { NewFeaturesKind } from '../users/types';
import { isStaff } from '../users/utils';
import { MailService } from './mailer';
@@ -23,7 +25,12 @@ export const NextAuthOptionsProvide = Symbol('NextAuthOptions');
export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
provide: NextAuthOptionsProvide,
useFactory(config: Config, prisma: PrismaService, mailer: MailService) {
useFactory(
config: Config,
prisma: PrismaService,
mailer: MailService,
session: SessionService
) {
const logger = new Logger('NextAuth');
const prismaAdapter = PrismaAdapter(prisma);
// createUser exists in the adapter
@@ -72,15 +79,31 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
from: config.auth.email.sender,
async sendVerificationRequest(params: SendVerificationRequestParams) {
const { identifier, url, provider } = params;
const { searchParams } = new URL(url);
const callbackUrl = searchParams.get('callbackUrl') || '';
const urlWithToken = new URL(url);
const callbackUrl =
urlWithToken.searchParams.get('callbackUrl') || '';
if (!callbackUrl) {
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,
from: provider.from,
});
const result = await mailer.sendSignInEmail(
urlWithToken.toString(),
{
to: identifier,
from: provider.from,
}
);
logger.log(
`send verification email success: ${result.accepted.join(', ')}`
);
@@ -277,5 +300,5 @@ export const NextAuthOptionsProvider: FactoryProvider<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')) {
this.logger.debug(req.headers);
if (!req.headers?.referer) {
res.redirect('https://community.affine.pro/c/insider-general/');
} else {
@@ -145,7 +144,6 @@ export class NextAuthController {
}
if (redirect) {
this.logger.debug(providerId, action, req.headers);
if (providerId === 'credentials') {
res.send(JSON.stringify({ ok: true, url: redirect }));
} else if (
+60 -18
View File
@@ -1,4 +1,8 @@
import { ForbiddenException, UseGuards } from '@nestjs/common';
import {
BadRequestException,
ForbiddenException,
UseGuards,
} from '@nestjs/common';
import {
Args,
Context,
@@ -10,11 +14,13 @@ import {
Resolver,
} from '@nestjs/graphql';
import type { Request } from 'express';
import { nanoid } from 'nanoid';
import { Config } from '../../config';
import { SessionService } from '../../session';
import { CloudThrottlerGuard, Throttle } from '../../throttler';
import { UserType } from '../users/resolver';
import { CurrentUser } from './guard';
import { Auth, CurrentUser } from './guard';
import { AuthService } from './service';
@ObjectType()
@@ -37,14 +43,15 @@ export class TokenType {
export class AuthResolver {
constructor(
private readonly config: Config,
private auth: AuthService
private auth: AuthService,
private readonly session: SessionService
) {}
@Throttle(20, 60)
@ResolveField(() => TokenType)
token(@CurrentUser() currentUser: UserType, @Parent() user: UserType) {
if (user.id !== currentUser.id) {
throw new ForbiddenException();
throw new BadRequestException('Invalid user');
}
return {
@@ -80,58 +87,93 @@ export class AuthResolver {
@Throttle(5, 60)
@Mutation(() => UserType)
@Auth()
async changePassword(
@Context() ctx: { req: Request },
@Args('id') id: string,
@CurrentUser() user: UserType,
@Args('token') token: string,
@Args('newPassword') newPassword: string
) {
const user = await this.auth.changePassword(id, newPassword);
ctx.req.user = user;
const id = await this.session.get(token);
if (!id || id !== user.id) {
throw new ForbiddenException('Invalid token');
}
await this.auth.changePassword(id, newPassword);
await this.session.delete(token);
return user;
}
@Throttle(5, 60)
@Mutation(() => UserType)
@Auth()
async changeEmail(
@Context() ctx: { req: Request },
@Args('id') id: string,
@CurrentUser() user: UserType,
@Args('token') token: string,
@Args('email') email: string
) {
const user = await this.auth.changeEmail(id, email);
ctx.req.user = user;
const id = await this.session.get(token);
if (!id || id !== user.id) {
throw new ForbiddenException('Invalid token');
}
await this.auth.changeEmail(id, email);
await this.session.delete(token);
return user;
}
@Throttle(5, 60)
@Mutation(() => Boolean)
@Auth()
async sendChangePasswordEmail(
@CurrentUser() user: UserType,
@Args('email') email: string,
@Args('callbackUrl') callbackUrl: string
) {
const url = `${this.config.baseUrl}${callbackUrl}`;
const res = await this.auth.sendChangePasswordEmail(email, url);
const token = nanoid();
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;
}
@Throttle(5, 60)
@Mutation(() => Boolean)
@Auth()
async sendSetPasswordEmail(
@CurrentUser() user: UserType,
@Args('email') email: string,
@Args('callbackUrl') callbackUrl: string
) {
const url = `${this.config.baseUrl}${callbackUrl}`;
const res = await this.auth.sendSetPasswordEmail(email, url);
const token = nanoid();
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;
}
@Throttle(5, 60)
@Mutation(() => Boolean)
@Auth()
async sendChangeEmail(
@CurrentUser() user: UserType,
@Args('email') email: string,
@Args('callbackUrl') callbackUrl: string
) {
const url = `${this.config.baseUrl}${callbackUrl}`;
const res = await this.auth.sendChangeEmail(email, url);
const token = nanoid();
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;
}
}
+1 -1
View File
@@ -91,7 +91,7 @@ export class UserResolver {
name: 'currentUser',
description: 'Get current user',
})
async currentUser(@CurrentUser() user: User) {
async currentUser(@CurrentUser() user: UserType) {
const storedUser = await this.prisma.user.findUnique({
where: { id: user.id },
});