mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
fix(server): use post request to consume magic link token (#6656)
This commit is contained in:
@@ -31,6 +31,11 @@ class SignInCredential {
|
||||
password?: string;
|
||||
}
|
||||
|
||||
class MagicLinkCredential {
|
||||
email!: string;
|
||||
token!: string;
|
||||
}
|
||||
|
||||
@Throttle('strict')
|
||||
@Controller('/api/auth')
|
||||
export class AuthController {
|
||||
@@ -90,7 +95,7 @@ export class AuthController {
|
||||
) {
|
||||
const token = await this.token.createToken(TokenType.SignIn, email);
|
||||
|
||||
const magicLink = this.url.link('/api/auth/magic-link', {
|
||||
const magicLink = this.url.link('/magic-link', {
|
||||
token,
|
||||
email,
|
||||
redirect_uri: redirectUri,
|
||||
@@ -129,20 +134,16 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('/magic-link')
|
||||
@Post('/magic-link')
|
||||
async magicLinkSignIn(
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
@Query('token') token?: string,
|
||||
@Query('email') email?: string,
|
||||
@Query('redirect_uri') redirectUri = this.url.home
|
||||
@Body() { email, token }: MagicLinkCredential
|
||||
) {
|
||||
if (!token || !email) {
|
||||
throw new BadRequestException('Invalid Sign-in mail Token');
|
||||
throw new BadRequestException('Missing sign-in mail token');
|
||||
}
|
||||
|
||||
email = decodeURIComponent(email);
|
||||
token = decodeURIComponent(token);
|
||||
validators.assertValidEmail(email);
|
||||
|
||||
const valid = await this.token.verifyToken(TokenType.SignIn, token, {
|
||||
@@ -150,7 +151,7 @@ export class AuthController {
|
||||
});
|
||||
|
||||
if (!valid) {
|
||||
throw new BadRequestException('Invalid Sign-in mail Token');
|
||||
throw new BadRequestException('Invalid sign-in mail token');
|
||||
}
|
||||
|
||||
const user = await this.user.fulfillUser(email, {
|
||||
@@ -160,7 +161,7 @@ export class AuthController {
|
||||
|
||||
await this.auth.setCookie(req, res, user);
|
||||
|
||||
return this.url.safeRedirect(res, redirectUri);
|
||||
res.send({ id: user.id, email: user.email, name: user.name });
|
||||
}
|
||||
|
||||
@Throttle('default', { limit: 1200 })
|
||||
|
||||
@@ -70,14 +70,17 @@ export class TokenService {
|
||||
!expired && (!record.credential || record.credential === credential);
|
||||
|
||||
if ((expired || valid) && !keep) {
|
||||
await this.db.verificationToken.delete({
|
||||
const deleted = await this.db.verificationToken.deleteMany({
|
||||
where: {
|
||||
type_token: {
|
||||
token,
|
||||
type,
|
||||
},
|
||||
token,
|
||||
type,
|
||||
},
|
||||
});
|
||||
|
||||
// already deleted, means token has been used
|
||||
if (!deleted.count) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return valid ? record : null;
|
||||
|
||||
@@ -26,7 +26,7 @@ export function getRequestResponseFromHost(host: ArgumentsHost) {
|
||||
const ws = host.switchToWs();
|
||||
const req = ws.getClient<Socket>().client.conn.request as Request;
|
||||
|
||||
const cookieStr = req?.headers?.cookie;
|
||||
const cookieStr = req?.headers?.cookie ?? '';
|
||||
// patch cookies to match auth guard logic
|
||||
if (typeof cookieStr === 'string') {
|
||||
req.cookies = cookieStr.split(';').reduce(
|
||||
|
||||
@@ -69,13 +69,15 @@ test('should be able to sign in with email', async t => {
|
||||
t.is(res.body.email, u1.email);
|
||||
t.true(mailer.sendSignInMail.calledOnce);
|
||||
|
||||
let [signInLink] = mailer.sendSignInMail.firstCall.args;
|
||||
const [signInLink] = mailer.sendSignInMail.firstCall.args;
|
||||
const url = new URL(signInLink);
|
||||
signInLink = url.pathname + url.search;
|
||||
const email = url.searchParams.get('email');
|
||||
const token = url.searchParams.get('token');
|
||||
|
||||
const signInRes = await request(app.getHttpServer())
|
||||
.get(signInLink)
|
||||
.expect(302);
|
||||
.post('/api/auth/magic-link')
|
||||
.send({ email, token })
|
||||
.expect(201);
|
||||
|
||||
const session = await getSession(app, signInRes);
|
||||
t.is(session.user!.id, u1.id);
|
||||
@@ -95,13 +97,15 @@ test('should be able to sign up with email', async t => {
|
||||
t.is(res.body.email, 'u2@affine.pro');
|
||||
t.true(mailer.sendSignUpMail.calledOnce);
|
||||
|
||||
let [signUpLink] = mailer.sendSignUpMail.firstCall.args;
|
||||
const [signUpLink] = mailer.sendSignUpMail.firstCall.args;
|
||||
const url = new URL(signUpLink);
|
||||
signUpLink = url.pathname + url.search;
|
||||
const email = url.searchParams.get('email');
|
||||
const token = url.searchParams.get('token');
|
||||
|
||||
const signInRes = await request(app.getHttpServer())
|
||||
.get(signUpLink)
|
||||
.expect(302);
|
||||
.post('/api/auth/magic-link')
|
||||
.send({ email, token })
|
||||
.expect(201);
|
||||
|
||||
const session = await getSession(app, signInRes);
|
||||
t.is(session.user!.email, 'u2@affine.pro');
|
||||
|
||||
40
packages/frontend/core/src/pages/magic-link.tsx
Normal file
40
packages/frontend/core/src/pages/magic-link.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { type LoaderFunction, redirect } from 'react-router-dom';
|
||||
|
||||
export const loader: LoaderFunction = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const queries = url.searchParams;
|
||||
const email = queries.get('email');
|
||||
const token = queries.get('token');
|
||||
const redirectUri = queries.get('redirect_uri');
|
||||
|
||||
if (!email || !token) {
|
||||
return redirect('/404');
|
||||
}
|
||||
|
||||
const res = await fetch('/api/auth/magic-link', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, token }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let error: string;
|
||||
try {
|
||||
const { message } = await res.json();
|
||||
error = message;
|
||||
} catch (e) {
|
||||
error = 'failed to verify sign-in token';
|
||||
}
|
||||
return redirect(`/signIn?error=${encodeURIComponent(error)}`);
|
||||
}
|
||||
|
||||
location.href = redirectUri || '/';
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
// TODO: loading ui
|
||||
return null;
|
||||
};
|
||||
@@ -76,6 +76,10 @@ export const topLevelRoutes = [
|
||||
path: '/signIn',
|
||||
lazy: () => import('./pages/sign-in'),
|
||||
},
|
||||
{
|
||||
path: '/magic-link',
|
||||
lazy: () => import('./pages/magic-link'),
|
||||
},
|
||||
{
|
||||
path: '/open-app/:action',
|
||||
lazy: () => import('./pages/open-app'),
|
||||
|
||||
Reference in New Issue
Block a user