feat: 实现演示验证码和令牌轮换
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Post,
|
||||
Req,
|
||||
Res,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import type { Request, Response } from "express";
|
||||
import { parse as parseCookie } from "cookie";
|
||||
import { ErrorCode } from "@drift/contracts";
|
||||
import { DomainException } from "../common/domain.exception.js";
|
||||
import { PrismaService } from "../database/prisma.service.js";
|
||||
import { AuthGuard } from "./auth.guard.js";
|
||||
import { AuthService, type TokenPair } from "./auth.service.js";
|
||||
import { CurrentUser } from "./current-user.decorator.js";
|
||||
import type { AccessClaims } from "./token.service.js";
|
||||
import { LoginDto, SendSmsDto } from "./dto.js";
|
||||
const COOKIE = "refresh_token";
|
||||
@Controller()
|
||||
export class AuthController {
|
||||
constructor(
|
||||
@Inject(AuthService) private readonly auth: AuthService,
|
||||
@Inject(PrismaService) private readonly prisma: PrismaService,
|
||||
) {}
|
||||
@Post("auth/sms/send") send(@Body() dto: SendSmsDto, @Req() req: Request) {
|
||||
return this.auth.sendSms(dto.phone, dto.deviceId, this.ip(req));
|
||||
}
|
||||
@Post("auth/sms/login") async login(
|
||||
@Body() dto: LoginDto,
|
||||
@Req() req: Request,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
) {
|
||||
const pair = await this.auth.login(
|
||||
dto.phone,
|
||||
dto.code,
|
||||
dto.deviceId,
|
||||
this.ip(req),
|
||||
);
|
||||
this.cookie(res, pair.refreshToken);
|
||||
return this.publicPair(pair);
|
||||
}
|
||||
@Post("auth/token/refresh") @HttpCode(HttpStatus.OK) async refresh(
|
||||
@Req() req: Request,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
) {
|
||||
this.origin(req);
|
||||
const pair = await this.auth.refresh(this.readCookie(req));
|
||||
this.cookie(res, pair.refreshToken);
|
||||
return this.publicPair(pair);
|
||||
}
|
||||
@Post("auth/logout") @HttpCode(HttpStatus.OK) async logout(
|
||||
@Req() req: Request,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
) {
|
||||
this.origin(req);
|
||||
await this.auth.logout(this.readCookieOptional(req));
|
||||
res.clearCookie(COOKIE, {
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
path: "/api/v1/auth",
|
||||
});
|
||||
return { loggedOut: true };
|
||||
}
|
||||
@Get("me") @UseGuards(AuthGuard) async me(@CurrentUser() user: AccessClaims) {
|
||||
const account = await this.prisma.account.findUniqueOrThrow({
|
||||
where: { id: user.sub },
|
||||
include: { anonymousProfile: true },
|
||||
});
|
||||
return {
|
||||
accountId: account.id,
|
||||
publicId: account.anonymousProfile?.publicId ?? null,
|
||||
nickname: account.anonymousProfile?.nickname ?? null,
|
||||
};
|
||||
}
|
||||
private publicPair(pair: TokenPair) {
|
||||
return {
|
||||
accessToken: pair.accessToken,
|
||||
expiresIn: pair.expiresIn,
|
||||
tokenType: "Bearer",
|
||||
};
|
||||
}
|
||||
private cookie(res: Response, value: string) {
|
||||
res.cookie(COOKIE, value, {
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
path: "/api/v1/auth",
|
||||
maxAge: Number(process.env.REFRESH_TOKEN_TTL_SECONDS ?? 2592000) * 1000,
|
||||
});
|
||||
}
|
||||
private origin(req: Request) {
|
||||
const origin = req.headers.origin;
|
||||
if (origin !== process.env.WEB_ORIGIN)
|
||||
throw new DomainException(
|
||||
ErrorCode.AUTH_ORIGIN_FORBIDDEN,
|
||||
"Forbidden origin",
|
||||
HttpStatus.FORBIDDEN,
|
||||
);
|
||||
}
|
||||
private readCookieOptional(req: Request) {
|
||||
const raw = req.headers.cookie;
|
||||
if (!raw) return undefined;
|
||||
try {
|
||||
return parseCookie(raw)[COOKIE];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
private readCookie(req: Request) {
|
||||
const value = this.readCookieOptional(req);
|
||||
if (!value)
|
||||
throw new DomainException(
|
||||
ErrorCode.AUTH_INVALID_CREDENTIALS,
|
||||
"Invalid credentials",
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
return value;
|
||||
}
|
||||
private ip(req: Request) {
|
||||
return req.ip || req.socket.remoteAddress || "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
Reflect.defineMetadata(
|
||||
"design:paramtypes",
|
||||
[SendSmsDto, Object],
|
||||
AuthController.prototype,
|
||||
"send",
|
||||
);
|
||||
Reflect.defineMetadata(
|
||||
"design:paramtypes",
|
||||
[LoginDto, Object, Object],
|
||||
AuthController.prototype,
|
||||
"login",
|
||||
);
|
||||
Reference in New Issue
Block a user