-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauth.service.ts
More file actions
98 lines (85 loc) · 2.63 KB
/
auth.service.ts
File metadata and controls
98 lines (85 loc) · 2.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import { cacheAsync } from "decorator-toolkit/cache-async/legacy";
import { rateLimit } from "decorator-toolkit/rate-limit/legacy";
import { timeout } from "decorator-toolkit/timeout/legacy";
import {
jwtVerify,
SignJWT,
} from "jose";
import {
DataSource,
Repository,
} from "typeorm";
import {
ConflictException,
Injectable,
UnauthorizedException,
} from "../../src/index.js";
import { User } from "./entities/user.entity.js";
const JWT_SECRET = new TextEncoder().encode("demo-secret-change-in-production");
const JWT_ALGORITHM = "HS256";
const JWT_EXPIRY = "1h";
@Injectable()
export class AuthService {
#repo: Repository<User>;
constructor(dataSource: DataSource) {
this.#repo = dataSource.getRepository(User);
}
// Limit registration attempts: max 5 globally per minute
@rateLimit<AuthService, [string, string]>({
allowedCalls: 5,
timeSpanMs: 60_000,
keyResolver: (_email, _password) => "register",
})
async register(email: string, password: string): Promise<string> {
const existing = await this.#repo.findOneBy({ email: email });
if (existing) {
throw new ConflictException("Email already registered");
}
const passwordHash = await Bun.password.hash(password);
const user = this.#repo.create({ email: email, passwordHash: passwordHash });
await this.#repo.save(user);
return this.#signToken(user.id);
}
// Limit login attempts: max 10 per email address per minute
@rateLimit<AuthService, [string, string]>({
allowedCalls: 10,
timeSpanMs: 60_000,
keyResolver: (email) => email,
})
async login(email: string, password: string): Promise<string> {
const user = await this.#repo.findOneBy({ email: email });
if (!user) {
throw new UnauthorizedException("Invalid credentials");
}
const valid = await Bun.password.verify(password, user.passwordHash);
if (!valid) {
throw new UnauthorizedException("Invalid credentials");
}
return this.#signToken(user.id);
}
// JWT verification should be fast; abort if it takes over 500ms
@timeout(500)
async verifyToken(token: string): Promise<string | null> {
try {
const { payload } = await jwtVerify(token, JWT_SECRET, {
algorithms: [JWT_ALGORITHM],
});
return (payload.sub as string) ?? null;
} catch {
return null;
}
}
// Cache repeated profile lookups for the same user ID for 30s
@cacheAsync({ ttlMs: 30_000 })
async findById(id: string): Promise<User | null> {
return this.#repo.findOneBy({ id: id });
}
async #signToken(userId: string): Promise<string> {
return new SignJWT({})
.setProtectedHeader({ alg: JWT_ALGORITHM })
.setSubject(userId)
.setIssuedAt()
.setExpirationTime(JWT_EXPIRY)
.sign(JWT_SECRET);
}
}