@@ -98,6 +98,7 @@ export class AuthResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.auth.changePassword(user.id, newPassword);
|
await this.auth.changePassword(user.id, newPassword);
|
||||||
|
await this.auth.revokeUserSessions(user.id);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@@ -121,6 +122,7 @@ export class AuthResolver {
|
|||||||
email = decodeURIComponent(email);
|
email = decodeURIComponent(email);
|
||||||
|
|
||||||
await this.auth.changeEmail(user.id, email);
|
await this.auth.changeEmail(user.id, email);
|
||||||
|
await this.auth.revokeUserSessions(user.id);
|
||||||
await this.auth.sendNotificationChangeEmail(email);
|
await this.auth.sendNotificationChangeEmail(email);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
|||||||
@@ -354,6 +354,15 @@ export class AuthService implements OnApplicationBootstrap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async revokeUserSessions(userId: string, sessionId?: string) {
|
||||||
|
return this.db.userSession.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
sessionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async setCookie(_req: Request, res: Response, user: { id: string }) {
|
async setCookie(_req: Request, res: Response, user: { id: string }) {
|
||||||
const session = await this.createUserSession(
|
const session = await this.createUserSession(
|
||||||
user
|
user
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { randomBytes } from 'node:crypto';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getCurrentMailMessageCount,
|
getCurrentMailMessageCount,
|
||||||
getLatestMailMessage,
|
getTokenFromLatestMailMessage,
|
||||||
} from '@affine-test/kit/utils/cloud';
|
} from '@affine-test/kit/utils/cloud';
|
||||||
import type { INestApplication } from '@nestjs/common';
|
import type { INestApplication } from '@nestjs/common';
|
||||||
import type { TestFn } from 'ava';
|
import type { TestFn } from 'ava';
|
||||||
@@ -10,8 +12,11 @@ import { AuthService } from '../src/core/auth/service';
|
|||||||
import { MailService } from '../src/fundamentals/mailer';
|
import { MailService } from '../src/fundamentals/mailer';
|
||||||
import {
|
import {
|
||||||
changeEmail,
|
changeEmail,
|
||||||
|
changePassword,
|
||||||
createTestingApp,
|
createTestingApp,
|
||||||
|
currentUser,
|
||||||
sendChangeEmail,
|
sendChangeEmail,
|
||||||
|
sendSetPasswordEmail,
|
||||||
sendVerifyChangeEmail,
|
sendVerifyChangeEmail,
|
||||||
signUp,
|
signUp,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
@@ -40,7 +45,6 @@ test('change email', async t => {
|
|||||||
if (mail.hasConfigured()) {
|
if (mail.hasConfigured()) {
|
||||||
const u1Email = 'u1@affine.pro';
|
const u1Email = 'u1@affine.pro';
|
||||||
const u2Email = 'u2@affine.pro';
|
const u2Email = 'u2@affine.pro';
|
||||||
const tokenRegex = /token=3D([^"&]+)/;
|
|
||||||
|
|
||||||
const u1 = await signUp(app, 'u1', u1Email, '1');
|
const u1 = await signUp(app, 'u1', u1Email, '1');
|
||||||
|
|
||||||
@@ -54,12 +58,8 @@ test('change email', async t => {
|
|||||||
afterSendChangeMailCount,
|
afterSendChangeMailCount,
|
||||||
'failed to send change email'
|
'failed to send change email'
|
||||||
);
|
);
|
||||||
const changeEmailContent = await getLatestMailMessage();
|
|
||||||
|
|
||||||
const changeTokenMatch = changeEmailContent.Content.Body.match(tokenRegex);
|
const changeEmailToken = await getTokenFromLatestMailMessage();
|
||||||
const changeEmailToken = changeTokenMatch
|
|
||||||
? decodeURIComponent(changeTokenMatch[1].replace(/=\r\n/, ''))
|
|
||||||
: null;
|
|
||||||
|
|
||||||
t.not(
|
t.not(
|
||||||
changeEmailToken,
|
changeEmailToken,
|
||||||
@@ -82,12 +82,8 @@ test('change email', async t => {
|
|||||||
afterSendVerifyMailCount,
|
afterSendVerifyMailCount,
|
||||||
'failed to send verify email'
|
'failed to send verify email'
|
||||||
);
|
);
|
||||||
const verifyEmailContent = await getLatestMailMessage();
|
|
||||||
|
|
||||||
const verifyTokenMatch = verifyEmailContent.Content.Body.match(tokenRegex);
|
const verifyEmailToken = await getTokenFromLatestMailMessage();
|
||||||
const verifyEmailToken = verifyTokenMatch
|
|
||||||
? decodeURIComponent(verifyTokenMatch[1].replace(/=\r\n/, ''))
|
|
||||||
: null;
|
|
||||||
|
|
||||||
t.not(
|
t.not(
|
||||||
verifyEmailToken,
|
verifyEmailToken,
|
||||||
@@ -107,3 +103,116 @@ test('change email', async t => {
|
|||||||
}
|
}
|
||||||
t.pass();
|
t.pass();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('set and change password', async t => {
|
||||||
|
const { mail, app, auth } = t.context;
|
||||||
|
if (mail.hasConfigured()) {
|
||||||
|
const u1Email = 'u1@affine.pro';
|
||||||
|
|
||||||
|
const u1 = await signUp(app, 'u1', u1Email, '1');
|
||||||
|
|
||||||
|
const primitiveMailCount = await getCurrentMailMessageCount();
|
||||||
|
|
||||||
|
await sendSetPasswordEmail(app, u1.token.token, u1Email, 'affine.pro');
|
||||||
|
|
||||||
|
const afterSendSetMailCount = await getCurrentMailMessageCount();
|
||||||
|
|
||||||
|
t.is(
|
||||||
|
primitiveMailCount + 1,
|
||||||
|
afterSendSetMailCount,
|
||||||
|
'failed to send set email'
|
||||||
|
);
|
||||||
|
|
||||||
|
const setPasswordToken = await getTokenFromLatestMailMessage();
|
||||||
|
|
||||||
|
t.not(
|
||||||
|
setPasswordToken,
|
||||||
|
null,
|
||||||
|
'fail to get set password token from email content'
|
||||||
|
);
|
||||||
|
|
||||||
|
const newPassword = randomBytes(16).toString('hex');
|
||||||
|
const userId = await changePassword(
|
||||||
|
app,
|
||||||
|
u1.token.token,
|
||||||
|
setPasswordToken as string,
|
||||||
|
newPassword
|
||||||
|
);
|
||||||
|
t.is(u1.id, userId, 'failed to set password');
|
||||||
|
|
||||||
|
const ret = auth.signIn(u1Email, newPassword);
|
||||||
|
t.notThrowsAsync(ret, 'failed to check password');
|
||||||
|
t.is((await ret).id, u1.id, 'failed to check password');
|
||||||
|
}
|
||||||
|
t.pass();
|
||||||
|
});
|
||||||
|
test('should revoke token after change user identify', async t => {
|
||||||
|
const { mail, app, auth } = t.context;
|
||||||
|
if (mail.hasConfigured()) {
|
||||||
|
// change email
|
||||||
|
{
|
||||||
|
const u1Email = 'u1@affine.pro';
|
||||||
|
const u2Email = 'u2@affine.pro';
|
||||||
|
|
||||||
|
const u1 = await signUp(app, 'u1', u1Email, '1');
|
||||||
|
|
||||||
|
{
|
||||||
|
const user = await currentUser(app, u1.token.token);
|
||||||
|
t.is(user?.email, u1Email, 'failed to get current user');
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendChangeEmail(app, u1.token.token, u1Email, 'affine.pro');
|
||||||
|
|
||||||
|
const changeEmailToken = await getTokenFromLatestMailMessage();
|
||||||
|
await sendVerifyChangeEmail(
|
||||||
|
app,
|
||||||
|
u1.token.token,
|
||||||
|
changeEmailToken as string,
|
||||||
|
u2Email,
|
||||||
|
'affine.pro'
|
||||||
|
);
|
||||||
|
|
||||||
|
const verifyEmailToken = await getTokenFromLatestMailMessage();
|
||||||
|
await changeEmail(
|
||||||
|
app,
|
||||||
|
u1.token.token,
|
||||||
|
verifyEmailToken as string,
|
||||||
|
u2Email
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = await currentUser(app, u1.token.token);
|
||||||
|
t.is(user, null, 'token should be revoked');
|
||||||
|
|
||||||
|
const newUserSession = await auth.signIn(u2Email, '1');
|
||||||
|
t.is(newUserSession?.email, u2Email, 'failed to sign in with new email');
|
||||||
|
}
|
||||||
|
|
||||||
|
// change password
|
||||||
|
{
|
||||||
|
const u3Email = 'u3@affine.pro';
|
||||||
|
|
||||||
|
const u3 = await signUp(app, 'u1', u3Email, '1');
|
||||||
|
|
||||||
|
{
|
||||||
|
const user = await currentUser(app, u3.token.token);
|
||||||
|
t.is(user?.email, u3Email, 'failed to get current user');
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendSetPasswordEmail(app, u3.token.token, u3Email, 'affine.pro');
|
||||||
|
const token = await getTokenFromLatestMailMessage();
|
||||||
|
const newPassword = randomBytes(16).toString('hex');
|
||||||
|
await changePassword(app, u3.token.token, token as string, newPassword);
|
||||||
|
|
||||||
|
const user = await currentUser(app, u3.token.token);
|
||||||
|
t.is(user, null, 'token should be revoked');
|
||||||
|
|
||||||
|
const newUserSession = await auth.signIn(u3Email, newPassword);
|
||||||
|
t.is(
|
||||||
|
newUserSession?.email,
|
||||||
|
u3Email,
|
||||||
|
'failed to sign in with new password'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.pass();
|
||||||
|
});
|
||||||
|
|||||||
@@ -106,6 +106,53 @@ export async function sendChangeEmail(
|
|||||||
return res.body.data.sendChangeEmail;
|
return res.body.data.sendChangeEmail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function sendSetPasswordEmail(
|
||||||
|
app: INestApplication,
|
||||||
|
userToken: string,
|
||||||
|
email: string,
|
||||||
|
callbackUrl: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post(gql)
|
||||||
|
.auth(userToken, { type: 'bearer' })
|
||||||
|
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||||
|
.send({
|
||||||
|
query: `
|
||||||
|
mutation {
|
||||||
|
sendSetPasswordEmail(email: "${email}", callbackUrl: "${callbackUrl}")
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
return res.body.data.sendChangeEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changePassword(
|
||||||
|
app: INestApplication,
|
||||||
|
userToken: string,
|
||||||
|
token: string,
|
||||||
|
password: string
|
||||||
|
): Promise<string> {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post(gql)
|
||||||
|
.auth(userToken, { type: 'bearer' })
|
||||||
|
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||||
|
.send({
|
||||||
|
query: `
|
||||||
|
mutation changePassword($token: String!, $password: String!) {
|
||||||
|
changePassword(token: $token, newPassword: $password) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: { token, password },
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
console.log(JSON.stringify(res.body));
|
||||||
|
return res.body.data.changePassword.id;
|
||||||
|
}
|
||||||
|
|
||||||
export async function sendVerifyChangeEmail(
|
export async function sendVerifyChangeEmail(
|
||||||
app: INestApplication,
|
app: INestApplication,
|
||||||
userToken: string,
|
userToken: string,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { faker } from '@faker-js/faker';
|
|||||||
import { hash } from '@node-rs/argon2';
|
import { hash } from '@node-rs/argon2';
|
||||||
import type { BrowserContext, Cookie, Page } from '@playwright/test';
|
import type { BrowserContext, Cookie, Page } from '@playwright/test';
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
import type { Assertions } from 'ava';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export async function getCurrentMailMessageCount() {
|
export async function getCurrentMailMessageCount() {
|
||||||
@@ -26,6 +27,19 @@ export async function getLatestMailMessage() {
|
|||||||
return data.items[0];
|
return data.items[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getTokenFromLatestMailMessage<A extends Assertions>(
|
||||||
|
test?: A
|
||||||
|
) {
|
||||||
|
const tokenRegex = /token=3D([^"&]+)/;
|
||||||
|
const emailContent = await getLatestMailMessage();
|
||||||
|
const tokenMatch = emailContent.Content.Body.match(tokenRegex);
|
||||||
|
const token = tokenMatch
|
||||||
|
? decodeURIComponent(tokenMatch[1].replace(/=\r\n/, ''))
|
||||||
|
: null;
|
||||||
|
test?.truthy(token);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getLoginCookie(
|
export async function getLoginCookie(
|
||||||
context: BrowserContext
|
context: BrowserContext
|
||||||
): Promise<Cookie | undefined> {
|
): Promise<Cookie | undefined> {
|
||||||
|
|||||||
Reference in New Issue
Block a user