mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-06 03:49:56 +08: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;
|
password?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MagicLinkCredential {
|
||||||
|
email!: string;
|
||||||
|
token!: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Throttle('strict')
|
@Throttle('strict')
|
||||||
@Controller('/api/auth')
|
@Controller('/api/auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
@@ -90,7 +95,7 @@ export class AuthController {
|
|||||||
) {
|
) {
|
||||||
const token = await this.token.createToken(TokenType.SignIn, email);
|
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,
|
token,
|
||||||
email,
|
email,
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
@@ -129,20 +134,16 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@Get('/magic-link')
|
@Post('/magic-link')
|
||||||
async magicLinkSignIn(
|
async magicLinkSignIn(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
@Query('token') token?: string,
|
@Body() { email, token }: MagicLinkCredential
|
||||||
@Query('email') email?: string,
|
|
||||||
@Query('redirect_uri') redirectUri = this.url.home
|
|
||||||
) {
|
) {
|
||||||
if (!token || !email) {
|
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);
|
validators.assertValidEmail(email);
|
||||||
|
|
||||||
const valid = await this.token.verifyToken(TokenType.SignIn, token, {
|
const valid = await this.token.verifyToken(TokenType.SignIn, token, {
|
||||||
@@ -150,7 +151,7 @@ export class AuthController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!valid) {
|
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, {
|
const user = await this.user.fulfillUser(email, {
|
||||||
@@ -160,7 +161,7 @@ export class AuthController {
|
|||||||
|
|
||||||
await this.auth.setCookie(req, res, user);
|
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 })
|
@Throttle('default', { limit: 1200 })
|
||||||
|
|||||||
@@ -70,14 +70,17 @@ export class TokenService {
|
|||||||
!expired && (!record.credential || record.credential === credential);
|
!expired && (!record.credential || record.credential === credential);
|
||||||
|
|
||||||
if ((expired || valid) && !keep) {
|
if ((expired || valid) && !keep) {
|
||||||
await this.db.verificationToken.delete({
|
const deleted = await this.db.verificationToken.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
type_token: {
|
token,
|
||||||
token,
|
type,
|
||||||
type,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// already deleted, means token has been used
|
||||||
|
if (!deleted.count) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return valid ? record : null;
|
return valid ? record : null;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function getRequestResponseFromHost(host: ArgumentsHost) {
|
|||||||
const ws = host.switchToWs();
|
const ws = host.switchToWs();
|
||||||
const req = ws.getClient<Socket>().client.conn.request as Request;
|
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
|
// patch cookies to match auth guard logic
|
||||||
if (typeof cookieStr === 'string') {
|
if (typeof cookieStr === 'string') {
|
||||||
req.cookies = cookieStr.split(';').reduce(
|
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.is(res.body.email, u1.email);
|
||||||
t.true(mailer.sendSignInMail.calledOnce);
|
t.true(mailer.sendSignInMail.calledOnce);
|
||||||
|
|
||||||
let [signInLink] = mailer.sendSignInMail.firstCall.args;
|
const [signInLink] = mailer.sendSignInMail.firstCall.args;
|
||||||
const url = new URL(signInLink);
|
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())
|
const signInRes = await request(app.getHttpServer())
|
||||||
.get(signInLink)
|
.post('/api/auth/magic-link')
|
||||||
.expect(302);
|
.send({ email, token })
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
const session = await getSession(app, signInRes);
|
const session = await getSession(app, signInRes);
|
||||||
t.is(session.user!.id, u1.id);
|
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.is(res.body.email, 'u2@affine.pro');
|
||||||
t.true(mailer.sendSignUpMail.calledOnce);
|
t.true(mailer.sendSignUpMail.calledOnce);
|
||||||
|
|
||||||
let [signUpLink] = mailer.sendSignUpMail.firstCall.args;
|
const [signUpLink] = mailer.sendSignUpMail.firstCall.args;
|
||||||
const url = new URL(signUpLink);
|
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())
|
const signInRes = await request(app.getHttpServer())
|
||||||
.get(signUpLink)
|
.post('/api/auth/magic-link')
|
||||||
.expect(302);
|
.send({ email, token })
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
const session = await getSession(app, signInRes);
|
const session = await getSession(app, signInRes);
|
||||||
t.is(session.user!.email, 'u2@affine.pro');
|
t.is(session.user!.email, 'u2@affine.pro');
|
||||||
|
|||||||
@@ -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',
|
path: '/signIn',
|
||||||
lazy: () => import('./pages/sign-in'),
|
lazy: () => import('./pages/sign-in'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/magic-link',
|
||||||
|
lazy: () => import('./pages/magic-link'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/open-app/:action',
|
path: '/open-app/:action',
|
||||||
lazy: () => import('./pages/open-app'),
|
lazy: () => import('./pages/open-app'),
|
||||||
|
|||||||
Reference in New Issue
Block a user