Sign in verified accounts automatically and require eight-character passwords
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user