Sign in verified accounts automatically and require eight-character passwords
This commit is contained in:
@@ -12,6 +12,7 @@ import { timingSafeEqual } from 'node:crypto';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
PASSWORD_MIN_LENGTH,
|
||||
devicePatch,
|
||||
requestInput,
|
||||
spaceInput,
|
||||
@@ -676,12 +677,20 @@ export async function createApp(cfg: Config, options: { mailer?: Mailer } = {})
|
||||
secure: cfg.production,
|
||||
sameSite: 'lax',
|
||||
});
|
||||
const setAccountCookie = (reply: any, result: { token: string; expiresAt: number }) =>
|
||||
reply.setCookie('hop_account', result.token, {
|
||||
httpOnly: true,
|
||||
secure: cfg.production,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
expires: new Date(result.expiresAt),
|
||||
});
|
||||
app.get('/api/auth/me', async (req) => {
|
||||
const current = service.auth.session(req.cookies.hop_account);
|
||||
return {
|
||||
account: current ? service.auth.serialize(current.user) : null,
|
||||
registrationOpen: cfg.registrationMode === 'open',
|
||||
passwordMinLength: cfg.production ? 12 : 1,
|
||||
passwordMinLength: PASSWORD_MIN_LENGTH,
|
||||
};
|
||||
});
|
||||
app.post('/api/auth/register', async (req) => {
|
||||
@@ -705,9 +714,11 @@ export async function createApp(cfg: Config, options: { mailer?: Mailer } = {})
|
||||
input.setupToken,
|
||||
);
|
||||
});
|
||||
app.post('/api/auth/verify-email', async (req) =>
|
||||
service.auth.verify(tokenInput.parse(req.body).token),
|
||||
);
|
||||
app.post('/api/auth/verify-email', async (req, reply) => {
|
||||
const result = service.auth.verify(tokenInput.parse(req.body).token, req.cookies.hop_account);
|
||||
setAccountCookie(reply, result);
|
||||
return { ok: true, account: result.account };
|
||||
});
|
||||
app.post('/api/auth/verify-email-change', async (req) =>
|
||||
service.auth.verifyEmailChange(tokenInput.parse(req.body).token),
|
||||
);
|
||||
@@ -729,13 +740,7 @@ export async function createApp(cfg: Config, options: { mailer?: Mailer } = {})
|
||||
ip(req) ?? 'unknown',
|
||||
req.cookies.hop_account,
|
||||
);
|
||||
reply.setCookie('hop_account', result.token, {
|
||||
httpOnly: true,
|
||||
secure: cfg.production,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
expires: new Date(result.expiresAt),
|
||||
});
|
||||
setAccountCookie(reply, result);
|
||||
return { account: result.account };
|
||||
});
|
||||
app.post('/api/auth/logout', async (req, reply) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { randomUUID, timingSafeEqual } from 'node:crypto';
|
||||
import { and, eq, gt, isNull, inArray } from 'drizzle-orm';
|
||||
import argon2 from 'argon2';
|
||||
import type { Service } from './service.js';
|
||||
import type { Account } from '@hop/shared';
|
||||
import { PASSWORD_MIN_LENGTH, type Account } from '@hop/shared';
|
||||
import * as s from './db/schema.js';
|
||||
import { fail, hash, randomToken } from './security.js';
|
||||
export type Mail = { to: string; subject: string; text: string; idempotencyKey?: string };
|
||||
@@ -83,7 +83,7 @@ export class Auth {
|
||||
return current!;
|
||||
}
|
||||
async password(value: string) {
|
||||
const minimum = this.service.cfg.production ? 12 : 1;
|
||||
const minimum = PASSWORD_MIN_LENGTH;
|
||||
if (Array.from(value).length < minimum || value.length > 1024)
|
||||
fail(400, 'PASSWORD_INVALID', `비밀번호는 ${minimum}~1024자로 입력해 주세요.`);
|
||||
return argon2.hash(value, passwordOptions);
|
||||
@@ -208,14 +208,19 @@ export class Auth {
|
||||
return { ok: true };
|
||||
});
|
||||
}
|
||||
verify(token: string) {
|
||||
return this.consume(token, 'verify', (_t, user) => {
|
||||
verify(token: string, oldToken?: string) {
|
||||
const previous = this.session(oldToken);
|
||||
let login!: ReturnType<Auth['createSession']>;
|
||||
const verified = this.consume(token, 'verify', (_t, user) => {
|
||||
this.service.db
|
||||
.update(s.users)
|
||||
.set({ status: 'active', verifiedAt: Date.now() })
|
||||
.where(eq(s.users.id, user.id))
|
||||
.run();
|
||||
login = this.createSession(this.user(user.id), oldToken);
|
||||
});
|
||||
if (previous) this.endSession(previous);
|
||||
return { ...verified, ...login };
|
||||
}
|
||||
async login(email: string, password: string, ip: string, oldToken?: string) {
|
||||
const normalized = normalizeEmail(email);
|
||||
@@ -228,6 +233,12 @@ export class Auth {
|
||||
.catch(() => false);
|
||||
if (!valid || !user || !['pending', 'active'].includes(user.status))
|
||||
fail(401, 'LOGIN_FAILED', '이메일 또는 비밀번호를 확인해 주세요.');
|
||||
const previous = this.session(oldToken);
|
||||
const result = this.createSession(user!, oldToken);
|
||||
if (previous) this.endSession(previous);
|
||||
return result;
|
||||
}
|
||||
private createSession(user: User, oldToken?: string) {
|
||||
const token = randomToken(),
|
||||
now = Date.now();
|
||||
this.service.db.transaction(() => {
|
||||
@@ -241,7 +252,7 @@ export class Auth {
|
||||
.insert(s.accountSessions)
|
||||
.values({
|
||||
id: randomUUID(),
|
||||
userId: user!.id,
|
||||
userId: user.id,
|
||||
tokenHash: hash(token),
|
||||
createdAt: now,
|
||||
lastSeen: now,
|
||||
@@ -249,22 +260,28 @@ export class Auth {
|
||||
})
|
||||
.run();
|
||||
});
|
||||
return { token, account: this.serialize(user!), expiresAt: now + 30 * 86400000 };
|
||||
return { token, account: this.serialize(user), expiresAt: now + 30 * 86400000 };
|
||||
}
|
||||
private endSession(current: NonNullable<ReturnType<Auth['session']>>) {
|
||||
this.service.storage.abortSession(current.session.id);
|
||||
for (const socket of this.service.io?.sockets.sockets.values() ?? []) {
|
||||
if (socket.data.accountSessionId !== current.session.id) continue;
|
||||
socket.leave(`account:${current.user.id}`);
|
||||
socket.leave(`account-session:${current.session.id}`);
|
||||
socket.data.accountId = undefined;
|
||||
socket.data.accountSessionId = undefined;
|
||||
socket.emit('account:revoked', {});
|
||||
}
|
||||
}
|
||||
logout(token: string | undefined) {
|
||||
const current = this.session(token);
|
||||
if (!current) return;
|
||||
this.service.storage.abortSession(current.session.id);
|
||||
this.service.db
|
||||
.update(s.accountSessions)
|
||||
.set({ revokedAt: Date.now() })
|
||||
.where(eq(s.accountSessions.id, current.session.id))
|
||||
.run();
|
||||
this.service.storage.abortSession(current.session.id);
|
||||
this.service.io?.to(`account-session:${current.session.id}`).emit('account:revoked', {});
|
||||
this.service.io
|
||||
?.in(`account-session:${current.session.id}`)
|
||||
.socketsLeave(`account:${current.user.id}`);
|
||||
this.endSession(current);
|
||||
}
|
||||
revoke(userId: string) {
|
||||
this.service.storage.abortUser(userId);
|
||||
|
||||
282
apps/server/test/auth-verification.test.js
Normal file
282
apps/server/test/auth-verification.test.js
Normal file
@@ -0,0 +1,282 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import argon2 from 'argon2';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { fixture } from './helpers.js';
|
||||
import * as s from '../src/db/schema.js';
|
||||
import { hash } from '../src/security.js';
|
||||
|
||||
const password = '12345678';
|
||||
async function setup() {
|
||||
const mails = [];
|
||||
const f = await fixture(false, async (mail) => mails.push(mail));
|
||||
f.cfg.registrationMode = 'open';
|
||||
f.cfg.sessionRate = 60;
|
||||
return { ...f, mails };
|
||||
}
|
||||
function useCookie(client, response) {
|
||||
const cookie = String(response.headers['set-cookie']).split(';')[0];
|
||||
assert(cookie.startsWith('hop_account='));
|
||||
client.cookie = client.cookie.replace(/;\s*hop_account=[^;]*/g, '') + '; ' + cookie;
|
||||
return cookie.slice('hop_account='.length);
|
||||
}
|
||||
async function register(f, email) {
|
||||
const client = await f.create();
|
||||
const response = await f.call(client, '/auth/register', 'POST', {
|
||||
email,
|
||||
password,
|
||||
passwordConfirmation: password,
|
||||
displayName: email.split('@')[0],
|
||||
});
|
||||
assert.equal(response.statusCode, 200, response.body);
|
||||
const token = f.mails.find((mail) => mail.to === email).text.match(/#verify=([A-Za-z0-9_-]+)/)[1];
|
||||
return { ...client, token, email };
|
||||
}
|
||||
async function verify(f, client, token = client.token) {
|
||||
const response = await f.call(client, '/auth/verify-email', 'POST', { token });
|
||||
assert.equal(response.statusCode, 200, response.body);
|
||||
useCookie(client, response);
|
||||
client.socket.disconnect();
|
||||
await f.connect(client);
|
||||
return response;
|
||||
}
|
||||
|
||||
test('가입 7자 거절·8자 허용, 기존 짧은 비밀번호 로그인은 유지', async () => {
|
||||
const f = await setup();
|
||||
try {
|
||||
const client = await f.create();
|
||||
const input = {
|
||||
email: 'length@hop.test',
|
||||
password: '1234567',
|
||||
passwordConfirmation: '1234567',
|
||||
displayName: 'length',
|
||||
};
|
||||
const rejected = await f.call(client, '/auth/register', 'POST', input);
|
||||
assert.equal(rejected.statusCode, 400, rejected.body);
|
||||
assert.equal(rejected.json().code, 'PASSWORD_INVALID');
|
||||
assert.equal(f.mails.length, 0);
|
||||
assert.equal(f.service.db.select().from(s.users).all().length, 0);
|
||||
const accepted = await f.call(client, '/auth/register', 'POST', {
|
||||
...input,
|
||||
password,
|
||||
passwordConfirmation: password,
|
||||
});
|
||||
assert.equal(accepted.statusCode, 200, accepted.body);
|
||||
assert.equal(f.mails.length, 1);
|
||||
const id = randomUUID();
|
||||
f.service.db
|
||||
.insert(s.users)
|
||||
.values({
|
||||
id,
|
||||
email: 'legacy@hop.test',
|
||||
displayName: 'legacy',
|
||||
passwordHash: await argon2.hash('1234'),
|
||||
status: 'active',
|
||||
verifiedAt: Date.now(),
|
||||
createdAt: Date.now(),
|
||||
})
|
||||
.run();
|
||||
assert.equal(
|
||||
(await f.call(client, '/auth/login', 'POST', { email: 'legacy@hop.test', password: '1234' }))
|
||||
.statusCode,
|
||||
200,
|
||||
);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
|
||||
for (const secure of [false, true]) {
|
||||
test(`인증 완료 후 비밀번호 재입력 없이 로그인·쿠키 보호·기기 유지 (Secure=${secure})`, async () => {
|
||||
const f = await setup();
|
||||
try {
|
||||
const a = await register(f, 'verify@hop.test');
|
||||
const device = (await f.call(a, '/device/me')).json().device;
|
||||
assert.equal((await f.call(a, '/auth/me')).json().account, null);
|
||||
f.cfg.production = secure;
|
||||
const response = await verify(f, a);
|
||||
assert.equal(response.json().account.email, a.email);
|
||||
assert.equal(response.json().account.verified, true);
|
||||
assert.equal(response.json().token, undefined);
|
||||
const header = String(response.headers['set-cookie']);
|
||||
assert.match(header, /HttpOnly/);
|
||||
assert.match(header, /SameSite=Lax/);
|
||||
assert.equal(/; Secure(?:;|$)/.test(header), secure);
|
||||
assert.equal((await f.call(a, '/auth/me')).json().account.email, a.email);
|
||||
assert.equal((await f.call(a, '/inbox')).statusCode, 200);
|
||||
assert.equal((await f.call(a, '/device/me')).json().device.id, device.id);
|
||||
const sessions = f.service.db.select().from(s.accountSessions).all();
|
||||
assert.equal(sessions.length, 1);
|
||||
assert.equal(sessions[0].tokenHash === a.cookie.split('hop_account=')[1], false);
|
||||
assert(sessions[0].expiresAt - sessions[0].createdAt <= 30 * 86400000);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('다른 계정 인증 시 현재 세션·업로드·소켓 종료, 다른 기기 세션과 기기 ID 유지·보관함 격리', async () => {
|
||||
const f = await setup();
|
||||
try {
|
||||
const a = await register(f, 'previous@hop.test');
|
||||
await verify(f, a);
|
||||
const previousToken = a.cookie.split('hop_account=')[1];
|
||||
const previous = f.service.auth.session(previousToken);
|
||||
const oldClient = { ...a };
|
||||
const own = await f.call(
|
||||
a,
|
||||
'/vault',
|
||||
'POST',
|
||||
{ kind: 'url', urls: ['https://example.com/previous-private'] },
|
||||
randomUUID(),
|
||||
);
|
||||
assert.equal(own.statusCode, 200, own.body);
|
||||
const second = await f.create();
|
||||
const anotherLogin = await f.call(second, '/auth/login', 'POST', { email: a.email, password });
|
||||
useCookie(second, anotherLogin);
|
||||
const b = await register(f, 'next@hop.test');
|
||||
let aborted;
|
||||
const abort = f.service.storage.abortSession.bind(f.service.storage);
|
||||
f.service.storage.abortSession = (id) => {
|
||||
aborted = id;
|
||||
abort(id);
|
||||
};
|
||||
const revoked = new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(Error('session notification missing')), 2000);
|
||||
a.socket.once('account:revoked', () => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
const switched = await f.call(a, '/auth/verify-email', 'POST', { token: b.token });
|
||||
assert.equal(switched.statusCode, 200, switched.body);
|
||||
await revoked;
|
||||
const serverSocket = f.service.io.sockets.sockets.get(a.socket.id);
|
||||
assert.equal(serverSocket.data.accountSessionId, undefined);
|
||||
assert.equal(serverSocket.rooms.has(`account:${previous.user.id}`), false);
|
||||
assert.equal(aborted, previous.session.id);
|
||||
useCookie(a, switched);
|
||||
assert.equal((await f.call(a, '/auth/me')).json().account.email, b.email);
|
||||
assert.equal((await f.call(oldClient, '/auth/me')).json().account, null);
|
||||
assert.equal((await f.call(oldClient, '/vault')).statusCode, 401);
|
||||
assert.equal((await f.call(a, '/vault')).json().items.length, 0);
|
||||
assert.equal((await f.call(second, '/auth/me')).json().account.email, 'previous@hop.test');
|
||||
assert.equal(
|
||||
(await f.call(second, '/vault')).json().items[0].url,
|
||||
'https://example.com/previous-private',
|
||||
);
|
||||
assert.equal((await f.call(a, '/device/me')).json().device.id, a.deviceId);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
|
||||
for (const invalid of ['expired', 'suspended', 'deleted', 'unknown']) {
|
||||
test(`유효하지 않은 인증(${invalid})은 기존 로그인과 인증 토큰 상태를 변경하지 않음`, async () => {
|
||||
const f = await setup();
|
||||
try {
|
||||
const a = await register(f, 'old@hop.test');
|
||||
await verify(f, a);
|
||||
const b = await register(f, 'invalid@hop.test');
|
||||
const user = f.service.db.select().from(s.users).where(eq(s.users.email, b.email)).get();
|
||||
const tokenRow = f.service.db
|
||||
.select()
|
||||
.from(s.accountTokens)
|
||||
.where(eq(s.accountTokens.tokenHash, hash(b.token)))
|
||||
.get();
|
||||
if (invalid === 'expired')
|
||||
f.service.db
|
||||
.update(s.accountTokens)
|
||||
.set({ expiresAt: Date.now() - 1 })
|
||||
.where(eq(s.accountTokens.id, tokenRow.id))
|
||||
.run();
|
||||
if (['suspended', 'deleted'].includes(invalid))
|
||||
f.service.db.update(s.users).set({ status: invalid }).where(eq(s.users.id, user.id)).run();
|
||||
const response = await f.call(a, '/auth/verify-email', 'POST', {
|
||||
token: invalid === 'unknown' ? 'A'.repeat(43) : b.token,
|
||||
});
|
||||
assert.equal(response.statusCode, 400, response.body);
|
||||
assert.equal(response.headers['set-cookie'], undefined);
|
||||
assert.equal((await f.call(a, '/auth/me')).json().account.email, a.email);
|
||||
assert.equal(
|
||||
f.service.db.select().from(s.accountTokens).where(eq(s.accountTokens.id, tokenRow.id)).get()
|
||||
.usedAt,
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
f.service.db
|
||||
.select()
|
||||
.from(s.accountSessions)
|
||||
.where(eq(s.accountSessions.userId, user.id))
|
||||
.all().length,
|
||||
0,
|
||||
);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('동시·재사용 인증은 한 번만 로그인하고 기존 로그인 쿠키를 덮어쓰지 않음', async () => {
|
||||
const f = await setup();
|
||||
try {
|
||||
const a = await register(f, 'once@hop.test');
|
||||
const responses = await Promise.all([
|
||||
f.call(a, '/auth/verify-email', 'POST', { token: a.token }),
|
||||
f.call(a, '/auth/verify-email', 'POST', { token: a.token }),
|
||||
]);
|
||||
assert.deepEqual(responses.map((r) => r.statusCode).sort(), [200, 400]);
|
||||
const successful = responses.find((r) => r.statusCode === 200);
|
||||
useCookie(a, successful);
|
||||
assert.equal(f.service.db.select().from(s.accountSessions).all().length, 1);
|
||||
const replay = await f.call(a, '/auth/verify-email', 'POST', { token: a.token });
|
||||
assert.equal(replay.statusCode, 400);
|
||||
assert.equal(replay.headers['set-cookie'], undefined);
|
||||
assert.equal((await f.call(a, '/auth/me')).json().account.email, a.email);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('세션 발급 실패 시 인증·이전 로그아웃·토큰 소비 모두 롤백하고 같은 링크로 재시도 가능', async () => {
|
||||
const f = await setup();
|
||||
try {
|
||||
const a = await register(f, 'keep@hop.test');
|
||||
await verify(f, a);
|
||||
const b = await register(f, 'retry@hop.test');
|
||||
const original = f.service.auth.createSession.bind(f.service.auth);
|
||||
f.service.auth.createSession = (...args) => {
|
||||
original(...args);
|
||||
throw Error('test issuance failure');
|
||||
};
|
||||
const failed = await f.call(a, '/auth/verify-email', 'POST', { token: b.token });
|
||||
assert.equal(failed.statusCode, 500);
|
||||
assert.equal(failed.headers['set-cookie'], undefined);
|
||||
assert.equal((await f.call(a, '/auth/me')).json().account.email, a.email);
|
||||
const pending = f.service.db.select().from(s.users).where(eq(s.users.email, b.email)).get();
|
||||
assert.equal(pending.status, 'pending');
|
||||
assert.equal(pending.verifiedAt, null);
|
||||
assert.equal(
|
||||
f.service.db
|
||||
.select()
|
||||
.from(s.accountSessions)
|
||||
.where(eq(s.accountSessions.userId, pending.id))
|
||||
.all().length,
|
||||
0,
|
||||
);
|
||||
assert.equal(
|
||||
f.service.db
|
||||
.select()
|
||||
.from(s.accountTokens)
|
||||
.where(eq(s.accountTokens.tokenHash, hash(b.token)))
|
||||
.get().usedAt,
|
||||
null,
|
||||
);
|
||||
f.service.auth.createSession = original;
|
||||
await verify(f, a, b.token);
|
||||
assert.equal((await f.call(a, '/auth/me')).json().account.email, b.email);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
@@ -30,13 +30,13 @@ async function signed(f, client, email) {
|
||||
id,
|
||||
email,
|
||||
displayName: email,
|
||||
passwordHash: await f.service.auth.password('1234'),
|
||||
passwordHash: await f.service.auth.password('12345678'),
|
||||
status: 'active',
|
||||
verifiedAt: Date.now(),
|
||||
createdAt: Date.now(),
|
||||
})
|
||||
.run();
|
||||
const login = await f.call(client, '/auth/login', 'POST', { email, password: '1234' });
|
||||
const login = await f.call(client, '/auth/login', 'POST', { email, password: '12345678' });
|
||||
assert.equal(login.statusCode, 200);
|
||||
client.cookie += '; ' + String(login.headers['set-cookie']).split(';')[0];
|
||||
return id;
|
||||
|
||||
@@ -17,13 +17,13 @@ async function signIn(f, client) {
|
||||
id,
|
||||
email,
|
||||
displayName: email,
|
||||
passwordHash: await f.service.auth.password('1234'),
|
||||
passwordHash: await f.service.auth.password('12345678'),
|
||||
status: 'active',
|
||||
verifiedAt: Date.now(),
|
||||
createdAt: Date.now(),
|
||||
})
|
||||
.run();
|
||||
const response = await f.call(client, '/auth/login', 'POST', { email, password: '1234' });
|
||||
const response = await f.call(client, '/auth/login', 'POST', { email, password: '12345678' });
|
||||
assert.equal(response.statusCode, 200, response.body);
|
||||
client.cookie += '; ' + String(response.headers['set-cookie']).split(';')[0];
|
||||
client.socket.disconnect();
|
||||
|
||||
@@ -374,8 +374,12 @@ test('수락 직전 오프라인 검사 및 여러 탭 합산', async () => {
|
||||
await new Promise((r) => setTimeout(r, 35));
|
||||
assert.equal((await f.call(a, '/discovery/devices')).json().devices.length, 1);
|
||||
const id = (await f.request(a, b)).json().requestId;
|
||||
const disconnected = new Promise<void>((resolve) => {
|
||||
f.service.io!.sockets.sockets.get(second.id!)!.once('disconnect', () => resolve());
|
||||
});
|
||||
second.disconnect();
|
||||
await new Promise((r) => setTimeout(r, 35));
|
||||
await disconnected;
|
||||
await new Promise((r) => setTimeout(r, f.cfg.offlineGraceMs + 15));
|
||||
assert.equal((await f.call(a, '/discovery/devices')).json().devices.length, 0);
|
||||
assert.equal(
|
||||
(await f.call(b, `/transfer-requests/${id}/accept`, 'POST', {}, randomUUID())).statusCode,
|
||||
|
||||
@@ -7,16 +7,17 @@ import type { Mail } from '../src/auth.js';
|
||||
import { normalizeEmail } from '../src/auth.js';
|
||||
import * as s from '../src/db/schema.js';
|
||||
const password = 'correct horse battery staple';
|
||||
test('개발은 1자 비밀번호를 허용하고 운영은 12자 규칙을 유지', async () => {
|
||||
test('새 비밀번호는 개발·운영 모두 최소 8자이며 API 안내와 일치', async () => {
|
||||
const f = await fixture();
|
||||
try {
|
||||
assert.equal((await f.call(null, '/auth/me')).json().passwordMinLength, 1);
|
||||
assert.match(await f.service.auth.password('1'), /^\$argon2id\$/);
|
||||
assert.equal((await f.call(null, '/auth/me')).json().passwordMinLength, 8);
|
||||
await assert.rejects(() => f.service.auth.password('1234567'), /8~1024/);
|
||||
assert.match(await f.service.auth.password('12345678'), /^\$argon2id\$/);
|
||||
await assert.rejects(() => f.service.auth.password(''));
|
||||
f.cfg.production = true;
|
||||
assert.equal((await f.call(null, '/auth/me')).json().passwordMinLength, 12);
|
||||
await assert.rejects(() => f.service.auth.password('1234'), /12~1024/);
|
||||
assert.match(await f.service.auth.password('123412341234'), /^\$argon2id\$/);
|
||||
assert.equal((await f.call(null, '/auth/me')).json().passwordMinLength, 8);
|
||||
await assert.rejects(() => f.service.auth.password('1234567'), /8~1024/);
|
||||
assert.match(await f.service.auth.password('12345678'), /^\$argon2id\$/);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
@@ -44,9 +45,9 @@ async function register(
|
||||
assert.equal(result.statusCode, 200, result.body);
|
||||
const mail = f.mails.find((m) => m.to === email)!;
|
||||
const token = mail.text.match(/#verify=([A-Za-z0-9_-]+)/)![1]!;
|
||||
if (verify)
|
||||
assert.equal((await f.call(c, '/auth/verify-email', 'POST', { token })).statusCode, 200);
|
||||
const login = await f.call(c, '/auth/login', 'POST', { email, password });
|
||||
const login = verify
|
||||
? await f.call(c, '/auth/verify-email', 'POST', { token })
|
||||
: await f.call(c, '/auth/login', 'POST', { email, password });
|
||||
assert.equal(login.statusCode, 200, login.body);
|
||||
c.cookie += '; ' + String(login.headers['set-cookie']).split(';')[0];
|
||||
c.socket?.disconnect();
|
||||
@@ -89,10 +90,13 @@ test('가입은 메일 게이트웨이로 인증 토큰을 보내고 해시만
|
||||
assert.match(user.passwordHash!, /^\$argon2id\$/);
|
||||
assert.equal(f.service.db.select().from(s.accountTokens).get()!.tokenHash === a.token, false);
|
||||
assert.equal((await request(f, a, 'alice@example.com')).statusCode, 403);
|
||||
assert.equal(
|
||||
(await f.call(a, '/auth/verify-email', 'POST', { token: a.token })).statusCode,
|
||||
200,
|
||||
);
|
||||
const verified = await f.call(a, '/auth/verify-email', 'POST', { token: a.token });
|
||||
assert.equal(verified.statusCode, 200, verified.body);
|
||||
a.cookie =
|
||||
a.cookie.replace(/;\s*hop_account=[^;]*/g, '') +
|
||||
'; ' +
|
||||
String(verified.headers['set-cookie']).split(';')[0];
|
||||
assert.equal((await f.call(a, '/auth/me')).json().account.verified, true);
|
||||
assert.equal(
|
||||
(await f.call(a, '/auth/verify-email', 'POST', { token: a.token })).statusCode,
|
||||
400,
|
||||
|
||||
@@ -15,13 +15,13 @@ async function signed(f, client, email) {
|
||||
id,
|
||||
email,
|
||||
displayName: email,
|
||||
passwordHash: await f.service.auth.password('1234'),
|
||||
passwordHash: await f.service.auth.password('12345678'),
|
||||
status: 'active',
|
||||
verifiedAt: Date.now(),
|
||||
createdAt: Date.now(),
|
||||
})
|
||||
.run();
|
||||
const login = await f.call(client, '/auth/login', 'POST', { email, password: '1234' });
|
||||
const login = await f.call(client, '/auth/login', 'POST', { email, password: '12345678' });
|
||||
assert.equal(login.statusCode, 200, login.body);
|
||||
client.cookie += '; ' + String(login.headers['set-cookie']).split(';')[0];
|
||||
return id;
|
||||
@@ -168,7 +168,7 @@ test('기기 수신은 로그인 계정의 날짜 기록에만 자동 저장,
|
||||
);
|
||||
const login = await f.call(b, '/auth/login', 'POST', {
|
||||
email: 'receiver@hop.test',
|
||||
password: '1234',
|
||||
password: '12345678',
|
||||
});
|
||||
b.cookie =
|
||||
b.cookie.split('; hop_account=')[0] +
|
||||
|
||||
Reference in New Issue
Block a user