mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-31 21:59:10 +08:00
feat: add user info edit verify (#4117)
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user