Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
530e1a3d5d
commit
195069450f
480 files changed
+15507
-24650
No files matched your search
@@ -1,8 +1,8 @@
|
||||
import { ADMIN_ROLES, TEACHER_ROLES } from "@oj2/contract"
|
||||
import type { Context, MiddlewareHandler } from "hono"
|
||||
import { ADMIN_ROLES, TEACHER_ROLES } from '@oj2/contract'
|
||||
import type { Context, MiddlewareHandler } from 'hono'
|
||||
|
||||
import { failure } from "../http"
|
||||
import { resolveSession, type AuthUser } from "./session"
|
||||
import { failure } from '../http'
|
||||
import { resolveSession, type AuthUser } from './session'
|
||||
|
||||
export interface AppEnv {
|
||||
Variables: {
|
||||
@@ -18,8 +18,8 @@ export interface AppEnv {
|
||||
*/
|
||||
export const optionalAuth: MiddlewareHandler<AppEnv> = async (c, next) => {
|
||||
const session = await resolveSession(c)
|
||||
if (session.reason === "expired") return denied(c, "expired")
|
||||
c.set("user", session.user)
|
||||
if (session.reason === 'expired') return denied(c, 'expired')
|
||||
c.set('user', session.user)
|
||||
await next()
|
||||
}
|
||||
|
||||
@@ -33,17 +33,17 @@ export const optionalAuth: MiddlewareHandler<AppEnv> = async (c, next) => {
|
||||
* 用 403 而不是 401:凭证是有效的,是这个账号不让用了,和 login 接口对禁用账号
|
||||
* 的回法(403 `account-disabled`)也一致。
|
||||
*/
|
||||
function denied(c: Context, reason: "anonymous" | "expired" | "disabled") {
|
||||
if (reason === "disabled") return failure(c, 403, "account-disabled", "账号已被禁用,请联系老师")
|
||||
function denied(c: Context, reason: 'anonymous' | 'expired' | 'disabled') {
|
||||
if (reason === 'disabled') return failure(c, 403, 'account-disabled', '账号已被禁用,请联系老师')
|
||||
// 前端见到这个码会拿 refresh token 换一张再重试,换不到才当没登录
|
||||
if (reason === "expired") return failure(c, 401, "token-expired", "登录已过期,请重新登录")
|
||||
return failure(c, 401, "login-required", "请先登录")
|
||||
if (reason === 'expired') return failure(c, 401, 'token-expired', '登录已过期,请重新登录')
|
||||
return failure(c, 401, 'login-required', '请先登录')
|
||||
}
|
||||
|
||||
export const requireAuth: MiddlewareHandler<AppEnv> = async (c, next) => {
|
||||
const session = await resolveSession(c)
|
||||
if (!session.user) return denied(c, session.reason)
|
||||
c.set("user", session.user)
|
||||
c.set('user', session.user)
|
||||
await next()
|
||||
}
|
||||
|
||||
@@ -58,8 +58,8 @@ function requireRole(allowed: (user: AuthUser) => boolean): MiddlewareHandler<Ap
|
||||
return async (c, next) => {
|
||||
const session = await resolveSession(c)
|
||||
if (!session.user) return denied(c, session.reason)
|
||||
if (!allowed(session.user)) return failure(c, 403, "permission-denied", "权限不足")
|
||||
c.set("user", session.user)
|
||||
if (!allowed(session.user)) return failure(c, 403, 'permission-denied', '权限不足')
|
||||
c.set('user', session.user)
|
||||
await next()
|
||||
}
|
||||
}
|
||||
@@ -71,13 +71,11 @@ export const requireAdmin = requireRole((user) => ADMIN_ROLES.includes(user.admi
|
||||
export const requireTeacher = requireRole((user) => TEACHER_ROLES.includes(user.adminType))
|
||||
|
||||
/** 旧 `@super_admin_required` */
|
||||
export const requireSuperAdmin = requireRole((user) => user.adminType === "Super Admin")
|
||||
export const requireSuperAdmin = requireRole((user) => user.adminType === 'Super Admin')
|
||||
|
||||
/**
|
||||
* 旧 `@problem_permission_required`:先要是管理员,再要 problem_permission 不为 None。
|
||||
* 注意它只管「能不能进这个接口」,「能改哪些题」(Own vs All)由各 handler 自己按
|
||||
* created_by 过滤 —— 旧后端也是这么分工的,别把两件事混在一起。
|
||||
*/
|
||||
export const requireProblemPermission = requireRole(
|
||||
(user) => ADMIN_ROLES.includes(user.adminType) && user.problemPermission !== "None",
|
||||
)
|
||||
export const requireProblemPermission = requireRole((user) => ADMIN_ROLES.includes(user.adminType) && user.problemPermission !== 'None')
|
||||
@@ -1,21 +1,21 @@
|
||||
import { pbkdf2, timingSafeEqual } from "node:crypto"
|
||||
import { promisify } from "node:util"
|
||||
import { pbkdf2, timingSafeEqual } from 'node:crypto'
|
||||
import { promisify } from 'node:util'
|
||||
|
||||
const pbkdf2Async = promisify(pbkdf2)
|
||||
|
||||
async function verifyDjangoPbkdf2(password: string, encoded: string) {
|
||||
const [algorithm, iterationsText, salt, digestText] = encoded.split("$")
|
||||
if (algorithm !== "pbkdf2_sha256" || !iterationsText || !salt || !digestText) {
|
||||
const [algorithm, iterationsText, salt, digestText] = encoded.split('$')
|
||||
if (algorithm !== 'pbkdf2_sha256' || !iterationsText || !salt || !digestText) {
|
||||
return false
|
||||
}
|
||||
|
||||
const iterations = Number(iterationsText)
|
||||
const expected = Buffer.from(digestText, "base64")
|
||||
const expected = Buffer.from(digestText, 'base64')
|
||||
if (!Number.isSafeInteger(iterations) || iterations <= 0 || expected.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
const actual = await pbkdf2Async(password, salt, iterations, expected.length, "sha256")
|
||||
const actual = await pbkdf2Async(password, salt, iterations, expected.length, 'sha256')
|
||||
return timingSafeEqual(actual, expected)
|
||||
}
|
||||
|
||||
@@ -25,14 +25,14 @@ async function verifyDjangoPbkdf2(password: string, encoded: string) {
|
||||
* 迭代次数是从哈希串里读的,所以 120000 到 1200000 的老哈希都验得了。
|
||||
*/
|
||||
export async function verifyPassword(password: string, encoded: string) {
|
||||
if (encoded.startsWith("pbkdf2_sha256$")) {
|
||||
if (encoded.startsWith('pbkdf2_sha256$')) {
|
||||
return {
|
||||
valid: await verifyDjangoPbkdf2(password, encoded),
|
||||
needsUpgrade: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (encoded.startsWith("$argon2")) {
|
||||
if (encoded.startsWith('$argon2')) {
|
||||
return {
|
||||
valid: await Bun.password.verify(password, encoded),
|
||||
needsUpgrade: false,
|
||||
@@ -53,7 +53,7 @@ export async function verifyPassword(password: string, encoded: string) {
|
||||
* 账号一个都不用迁移**,Bun.password.verify 读串里的参数验,改这里只影响此后新写的哈希。
|
||||
*/
|
||||
const ARGON2_OPTIONS = {
|
||||
algorithm: "argon2id",
|
||||
algorithm: 'argon2id',
|
||||
memoryCost: 19456,
|
||||
timeCost: 2,
|
||||
} as const
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ChainableCommander } from "ioredis"
|
||||
import type { ChainableCommander } from 'ioredis'
|
||||
|
||||
import { redis } from "../redis"
|
||||
import { calendarDay } from "../time"
|
||||
import { redis } from '../redis'
|
||||
import { calendarDay } from '../time'
|
||||
|
||||
/**
|
||||
* 「谁现在在线」。member = userId,score = 最后一次活动的毫秒时间戳。
|
||||
@@ -11,7 +11,7 @@ import { calendarDay } from "../time"
|
||||
* 写入一律搭在已有的 pipeline 上(登录、每个带鉴权的请求、WebSocket 巡检),
|
||||
* 不多一趟往返。
|
||||
*/
|
||||
const PRESENCE_KEY = "online-users"
|
||||
const PRESENCE_KEY = 'online-users'
|
||||
|
||||
/**
|
||||
* 多久没动就算离线。挂着页面不操作的人靠 sweepSessions 每 60 秒续一次
|
||||
@@ -33,10 +33,7 @@ const SEEN_TODAY_TTL_SECONDS = 2 * 24 * 60 * 60
|
||||
export function markOnline(pipeline: ChainableCommander, userId: number) {
|
||||
const now = Date.now()
|
||||
const seenKey = seenTodayKey(calendarDay(now))
|
||||
pipeline
|
||||
.zadd(PRESENCE_KEY, now, String(userId))
|
||||
.zadd(seenKey, now, String(userId))
|
||||
.expire(seenKey, SEEN_TODAY_TTL_SECONDS)
|
||||
pipeline.zadd(PRESENCE_KEY, now, String(userId)).zadd(seenKey, now, String(userId)).expire(seenKey, SEEN_TODAY_TTL_SECONDS)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,11 +45,7 @@ export function markOnline(pipeline: ChainableCommander, userId: number) {
|
||||
*/
|
||||
export async function onlineUserIds() {
|
||||
const cutoff = Date.now() - ONLINE_WINDOW_MS
|
||||
const results = await redis
|
||||
.pipeline()
|
||||
.zremrangebyscore(PRESENCE_KEY, "-inf", `(${cutoff}`)
|
||||
.zrange(PRESENCE_KEY, "0", "-1")
|
||||
.exec()
|
||||
const results = await redis.pipeline().zremrangebyscore(PRESENCE_KEY, '-inf', `(${cutoff}`).zrange(PRESENCE_KEY, '0', '-1').exec()
|
||||
const members = (results?.[1]?.[1] ?? []) as string[]
|
||||
return new Set(members.map(Number).filter(Number.isInteger))
|
||||
}
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
import { randomBytes, timingSafeEqual } from "node:crypto"
|
||||
import { randomBytes, timingSafeEqual } from 'node:crypto'
|
||||
|
||||
import {
|
||||
toAdminType,
|
||||
toProblemPermission,
|
||||
type AdminType,
|
||||
type AuthTokens,
|
||||
type ProblemPermission,
|
||||
WS_AUTH_PROTOCOL,
|
||||
} from "@oj2/contract"
|
||||
import { toAdminType, toProblemPermission, type AdminType, type AuthTokens, type ProblemPermission, WS_AUTH_PROTOCOL } from '@oj2/contract'
|
||||
|
||||
import { eq, sql } from "drizzle-orm"
|
||||
import type { Context } from "hono"
|
||||
import { sign, verify } from "hono/jwt"
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import type { Context } from 'hono'
|
||||
import { sign, verify } from 'hono/jwt'
|
||||
|
||||
import { config } from "../config"
|
||||
import { db, schema } from "../db"
|
||||
import { publishSessionRevoked, type SessionRevokedReason } from "../events"
|
||||
import { clearOnline, markOnline } from "./presence"
|
||||
import { redis } from "../redis"
|
||||
import { config } from '../config'
|
||||
import { db, schema } from '../db'
|
||||
import { publishSessionRevoked, type SessionRevokedReason } from '../events'
|
||||
import { clearOnline, markOnline } from './presence'
|
||||
import { redis } from '../redis'
|
||||
|
||||
/**
|
||||
* 登录态是两张 token:
|
||||
@@ -39,12 +32,12 @@ import { redis } from "../redis"
|
||||
* 续会话),没有多出往返。
|
||||
*/
|
||||
|
||||
const SESSION_PREFIX = "auth-session:"
|
||||
const SESSION_PREFIX = 'auth-session:'
|
||||
/**
|
||||
* 输过的比赛密码,一张会话一个 hash(field = 比赛 id)。不塞进会话本体:
|
||||
* 会话本体每换一次 token 就整份重写一次,两边一起读改写会互相冲掉。
|
||||
*/
|
||||
const CONTEST_PASSWORD_PREFIX = "auth-contest:"
|
||||
const CONTEST_PASSWORD_PREFIX = 'auth-contest:'
|
||||
|
||||
export const ACCESS_TOKEN_TTL_SECONDS = 15 * 60
|
||||
|
||||
@@ -65,17 +58,17 @@ const REFRESH_GRACE_MS = 60_000
|
||||
* env 文件里各补一行,漏了也不会起不来。要让全站所有人重新登录,删掉这个 key
|
||||
* 再重启 api 即可。
|
||||
*/
|
||||
const JWT_SECRET_KEY = "auth:jwt-secret"
|
||||
const JWT_SECRET_KEY = 'auth:jwt-secret'
|
||||
let jwtSecret: string | null = null
|
||||
|
||||
export async function loadJwtSecret() {
|
||||
await redis.set(JWT_SECRET_KEY, randomBytes(48).toString("base64url"), "NX")
|
||||
await redis.set(JWT_SECRET_KEY, randomBytes(48).toString('base64url'), 'NX')
|
||||
jwtSecret = await redis.get(JWT_SECRET_KEY)
|
||||
if (!jwtSecret) throw new Error("JWT 密钥没能写进 Redis")
|
||||
if (!jwtSecret) throw new Error('JWT 密钥没能写进 Redis')
|
||||
}
|
||||
|
||||
function secret() {
|
||||
if (!jwtSecret) throw new Error("JWT 密钥还没加载,入口要先 await loadJwtSecret()")
|
||||
if (!jwtSecret) throw new Error('JWT 密钥还没加载,入口要先 await loadJwtSecret()')
|
||||
return jwtSecret
|
||||
}
|
||||
|
||||
@@ -133,7 +126,7 @@ function sameSecret(a: string, b: string | null) {
|
||||
|
||||
/** `<sid>.<随机串>` 拆开。格式不对返回 null */
|
||||
function splitRefreshToken(token: string) {
|
||||
const dot = token.indexOf(".")
|
||||
const dot = token.indexOf('.')
|
||||
if (dot <= 0 || dot === token.length - 1) return null
|
||||
return { sid: token.slice(0, dot), secret: token.slice(dot + 1) }
|
||||
}
|
||||
@@ -170,28 +163,21 @@ end
|
||||
return 0`
|
||||
|
||||
async function compareAndSetSession(sid: string, expected: string, value: StoredSession) {
|
||||
const result = await redis.eval(
|
||||
COMPARE_AND_SET,
|
||||
1,
|
||||
sessionKey(sid),
|
||||
expected,
|
||||
JSON.stringify(value),
|
||||
config.sessionTtlSeconds,
|
||||
)
|
||||
const result = await redis.eval(COMPARE_AND_SET, 1, sessionKey(sid), expected, JSON.stringify(value), config.sessionTtlSeconds)
|
||||
return result === 1
|
||||
}
|
||||
|
||||
async function signAccessToken(claims: AccessClaims) {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
return sign({ ...claims, iat: now, exp: now + ACCESS_TOKEN_TTL_SECONDS }, secret(), "HS256")
|
||||
return sign({ ...claims, iat: now, exp: now + ACCESS_TOKEN_TTL_SECONDS }, secret(), 'HS256')
|
||||
}
|
||||
|
||||
/** 验签名和有效期,载荷形状不对也当无效。不碰 Redis 和库 */
|
||||
async function verifyAccessToken(token: string): Promise<AccessClaims | null> {
|
||||
try {
|
||||
const payload = await verify(token, secret(), "HS256")
|
||||
const payload = await verify(token, secret(), 'HS256')
|
||||
const { uid, sid, ver } = payload
|
||||
if (!Number.isInteger(uid) || typeof sid !== "string" || !Number.isInteger(ver)) return null
|
||||
if (!Number.isInteger(uid) || typeof sid !== 'string' || !Number.isInteger(ver)) return null
|
||||
return { uid: uid as number, sid, ver: ver as number }
|
||||
} catch {
|
||||
return null
|
||||
@@ -216,13 +202,9 @@ async function selectUser(userId: number) {
|
||||
return user ?? null
|
||||
}
|
||||
|
||||
export async function issueTokens(
|
||||
userId: number,
|
||||
tokenVersion: number,
|
||||
previousLogin: string | null = null,
|
||||
): Promise<AuthTokens> {
|
||||
const sid = randomBytes(16).toString("base64url")
|
||||
const refreshSecret = randomBytes(32).toString("base64url")
|
||||
export async function issueTokens(userId: number, tokenVersion: number, previousLogin: string | null = null): Promise<AuthTokens> {
|
||||
const sid = randomBytes(16).toString('base64url')
|
||||
const refreshSecret = randomBytes(32).toString('base64url')
|
||||
const value: StoredSession = {
|
||||
userId,
|
||||
tokenVersion,
|
||||
@@ -234,9 +216,7 @@ export async function issueTokens(
|
||||
}
|
||||
// 两条写进一个 pipeline:一个班四十号人同时登录时,两趟往返和一趟的差别
|
||||
// 全压在登录这一下上
|
||||
const pipeline = redis
|
||||
.pipeline()
|
||||
.set(sessionKey(sid), JSON.stringify(value), "EX", config.sessionTtlSeconds)
|
||||
const pipeline = redis.pipeline().set(sessionKey(sid), JSON.stringify(value), 'EX', config.sessionTtlSeconds)
|
||||
markOnline(pipeline, userId)
|
||||
await pipeline.exec()
|
||||
return {
|
||||
@@ -246,9 +226,7 @@ export async function issueTokens(
|
||||
}
|
||||
}
|
||||
|
||||
export type RefreshResult =
|
||||
| { tokens: AuthTokens; reason?: undefined }
|
||||
| { tokens: null; reason: "invalid" | "disabled" }
|
||||
export type RefreshResult = { tokens: AuthTokens; reason?: undefined } | { tokens: null; reason: 'invalid' | 'disabled' }
|
||||
|
||||
/**
|
||||
* 拿 refresh token 换一张新的 access token,同时轮换 refresh token。
|
||||
@@ -259,30 +237,27 @@ export type RefreshResult =
|
||||
*/
|
||||
export async function refreshTokens(refreshToken: string): Promise<RefreshResult> {
|
||||
const parts = splitRefreshToken(refreshToken)
|
||||
if (!parts) return { tokens: null, reason: "invalid" }
|
||||
if (!parts) return { tokens: null, reason: 'invalid' }
|
||||
|
||||
// 比对再写入失败(别的标签页抢先轮换了)就重读一遍,这时手里这张已经是「上一张」,
|
||||
// 第二轮走宽限期。三轮还落不了地只可能是被刷,直接拒
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
const stored = await readSessionRaw(parts.sid)
|
||||
if (!stored) return { tokens: null, reason: "invalid" }
|
||||
if (!stored) return { tokens: null, reason: 'invalid' }
|
||||
const session = stored.value
|
||||
|
||||
const current = sameSecret(parts.secret, session.refreshSecret)
|
||||
const inGrace =
|
||||
!current &&
|
||||
sameSecret(parts.secret, session.previousRefreshSecret) &&
|
||||
Date.now() - session.rotatedAt < REFRESH_GRACE_MS
|
||||
if (!current && !inGrace) return { tokens: null, reason: "invalid" }
|
||||
const inGrace = !current && sameSecret(parts.secret, session.previousRefreshSecret) && Date.now() - session.rotatedAt < REFRESH_GRACE_MS
|
||||
if (!current && !inGrace) return { tokens: null, reason: 'invalid' }
|
||||
|
||||
const user = await selectUser(session.userId)
|
||||
if (!user || user.tokenVersion !== session.tokenVersion) {
|
||||
await redis.del(sessionKey(parts.sid))
|
||||
return { tokens: null, reason: "invalid" }
|
||||
return { tokens: null, reason: 'invalid' }
|
||||
}
|
||||
if (user.isDisabled) {
|
||||
await redis.del(sessionKey(parts.sid))
|
||||
return { tokens: null, reason: "disabled" }
|
||||
return { tokens: null, reason: 'disabled' }
|
||||
}
|
||||
|
||||
const accessToken = await signAccessToken({
|
||||
@@ -301,7 +276,7 @@ export async function refreshTokens(refreshToken: string): Promise<RefreshResult
|
||||
}
|
||||
}
|
||||
|
||||
const refreshSecret = randomBytes(32).toString("base64url")
|
||||
const refreshSecret = randomBytes(32).toString('base64url')
|
||||
const rotated = await compareAndSetSession(parts.sid, stored.raw, {
|
||||
...session,
|
||||
refreshSecret,
|
||||
@@ -318,7 +293,7 @@ export async function refreshTokens(refreshToken: string): Promise<RefreshResult
|
||||
}
|
||||
}
|
||||
}
|
||||
return { tokens: null, reason: "invalid" }
|
||||
return { tokens: null, reason: 'invalid' }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,10 +307,7 @@ export async function destroySession(refreshToken: string) {
|
||||
if (!parts) return null
|
||||
const session = await readSession(parts.sid)
|
||||
if (!session) return null
|
||||
if (
|
||||
!sameSecret(parts.secret, session.refreshSecret) &&
|
||||
!sameSecret(parts.secret, session.previousRefreshSecret)
|
||||
) {
|
||||
if (!sameSecret(parts.secret, session.refreshSecret) && !sameSecret(parts.secret, session.previousRefreshSecret)) {
|
||||
return null
|
||||
}
|
||||
await redis.del(sessionKey(parts.sid), `${CONTEST_PASSWORD_PREFIX}${parts.sid}`)
|
||||
@@ -375,23 +347,20 @@ export async function revokeUserSessions(userId: number, reason: SessionRevokedR
|
||||
*/
|
||||
export type SessionResult =
|
||||
| { user: AuthUser; sid: string; tokenVersion: number; reason?: undefined }
|
||||
| { user: null; reason: "anonymous" | "expired" | "disabled" }
|
||||
| { user: null; reason: 'anonymous' | 'expired' | 'disabled' }
|
||||
|
||||
async function authenticateToken(token: string | null): Promise<SessionResult> {
|
||||
if (!token) return { user: null, reason: "anonymous" }
|
||||
if (!token) return { user: null, reason: 'anonymous' }
|
||||
const claims = await verifyAccessToken(token)
|
||||
if (!claims) return { user: null, reason: "expired" }
|
||||
if (!claims) return { user: null, reason: 'expired' }
|
||||
|
||||
// 会话还在不在、用户还在不在,两件事并行问。EXPIRE 顺手给会话续期:
|
||||
// 不发 refresh 的请求也该算活动
|
||||
const [alive, user] = await Promise.all([
|
||||
redis.expire(sessionKey(claims.sid), config.sessionTtlSeconds),
|
||||
selectUser(claims.uid),
|
||||
])
|
||||
const [alive, user] = await Promise.all([redis.expire(sessionKey(claims.sid), config.sessionTtlSeconds), selectUser(claims.uid)])
|
||||
if (alive !== 1 || !user || user.tokenVersion !== claims.ver) {
|
||||
return { user: null, reason: "expired" }
|
||||
return { user: null, reason: 'expired' }
|
||||
}
|
||||
if (user.isDisabled) return { user: null, reason: "disabled" }
|
||||
if (user.isDisabled) return { user: null, reason: 'disabled' }
|
||||
|
||||
// 在线状态就是搭在每个带鉴权的请求上记的,见 presence.ts
|
||||
const online = redis.pipeline()
|
||||
@@ -418,14 +387,12 @@ function bearerToken(header: string | null | undefined) {
|
||||
|
||||
/** 中间件用这个:要区分「没登录」「token 不能用」「已被禁用」 */
|
||||
export function resolveSession(c: Context) {
|
||||
return authenticateToken(bearerToken(c.req.header("authorization")))
|
||||
return authenticateToken(bearerToken(c.req.header('authorization')))
|
||||
}
|
||||
|
||||
/** WebSocket 握手:access token 借子协议带过来,见契约里 WS_AUTH_PROTOCOL 的注释 */
|
||||
export async function authenticateWebSocket(request: Request) {
|
||||
const protocols = (request.headers.get("sec-websocket-protocol") ?? "")
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
const protocols = (request.headers.get('sec-websocket-protocol') ?? '').split(',').map((value) => value.trim())
|
||||
if (protocols[0] !== WS_AUTH_PROTOCOL) return null
|
||||
const result = await authenticateToken(protocols[1] || null)
|
||||
return result.user ? result : null
|
||||
@@ -451,7 +418,7 @@ export async function touchSession(sid: string, userId: number) {
|
||||
|
||||
/** 当前请求那张 access token 的载荷,验不过返回 null */
|
||||
async function requestClaims(c: Context) {
|
||||
const token = bearerToken(c.req.header("authorization"))
|
||||
const token = bearerToken(c.req.header('authorization'))
|
||||
return token ? verifyAccessToken(token) : null
|
||||
}
|
||||
|
||||
@@ -463,11 +430,7 @@ export async function setContestPassword(c: Context, contestId: number, password
|
||||
const claims = await requestClaims(c)
|
||||
if (!claims) return false
|
||||
const key = `${CONTEST_PASSWORD_PREFIX}${claims.sid}`
|
||||
await redis
|
||||
.pipeline()
|
||||
.hset(key, String(contestId), password)
|
||||
.expire(key, config.sessionTtlSeconds)
|
||||
.exec()
|
||||
await redis.pipeline().hset(key, String(contestId), password).expire(key, config.sessionTtlSeconds).exec()
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user