feat(16-01): update auth middleware and services to resolve userId
- verifyApiKey returns { userId } | null instead of boolean
- verifyAccessToken returns { userId } | null instead of boolean
- Add getOrCreateUser upsert function in auth.service
- Add getOrCreateUncategorized helper in category.service
- requireAuth sets userId on Hono context for all 3 auth methods
- Remove GET bypass: all API routes require auth for userId resolution
- Keep bypass for /api/auth and /api/health paths
This commit is contained in:
@@ -7,53 +7,50 @@ type Db = typeof prodDb;
|
||||
|
||||
// ── Client Registration ──────────────────────────────────────────────
|
||||
|
||||
export function registerClient(
|
||||
export async function registerClient(
|
||||
db: Db = prodDb,
|
||||
clientName: string,
|
||||
redirectUris: string[],
|
||||
): { clientId: string } {
|
||||
): Promise<{ clientId: string }> {
|
||||
const clientId = randomUUID();
|
||||
const redirectUrisJson = JSON.stringify(redirectUris);
|
||||
|
||||
db.insert(oauthClients)
|
||||
.values({ clientId, clientName, redirectUris: redirectUrisJson })
|
||||
.run();
|
||||
await db
|
||||
.insert(oauthClients)
|
||||
.values({ clientId, clientName, redirectUris: redirectUrisJson });
|
||||
|
||||
return { clientId };
|
||||
}
|
||||
|
||||
export function getClient(db: Db = prodDb, clientId: string) {
|
||||
return (
|
||||
db
|
||||
.select()
|
||||
.from(oauthClients)
|
||||
.where(eq(oauthClients.clientId, clientId))
|
||||
.get() ?? null
|
||||
);
|
||||
export async function getClient(db: Db = prodDb, clientId: string) {
|
||||
const [record] = await db
|
||||
.select()
|
||||
.from(oauthClients)
|
||||
.where(eq(oauthClients.clientId, clientId));
|
||||
|
||||
return record ?? null;
|
||||
}
|
||||
|
||||
// ── Authorization Code ───────────────────────────────────────────────
|
||||
|
||||
export function createAuthorizationCode(
|
||||
export async function createAuthorizationCode(
|
||||
db: Db = prodDb,
|
||||
clientId: string,
|
||||
codeChallenge: string,
|
||||
codeChallengeMethod: string,
|
||||
redirectUri: string,
|
||||
): { code: string } {
|
||||
): Promise<{ code: string }> {
|
||||
const code = randomBytes(32).toString("hex");
|
||||
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
|
||||
|
||||
db.insert(oauthCodes)
|
||||
.values({
|
||||
code,
|
||||
clientId,
|
||||
codeChallenge,
|
||||
codeChallengeMethod,
|
||||
redirectUri,
|
||||
expiresAt,
|
||||
})
|
||||
.run();
|
||||
await db.insert(oauthCodes).values({
|
||||
code,
|
||||
clientId,
|
||||
codeChallenge,
|
||||
codeChallengeMethod,
|
||||
redirectUri,
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
return { code };
|
||||
}
|
||||
@@ -64,16 +61,16 @@ export async function exchangeCode(
|
||||
codeVerifier: string,
|
||||
clientId: string,
|
||||
redirectUri: string,
|
||||
userId: number,
|
||||
): Promise<{
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresIn: number;
|
||||
} | null> {
|
||||
const record = db
|
||||
const [record] = await db
|
||||
.select()
|
||||
.from(oauthCodes)
|
||||
.where(eq(oauthCodes.code, code))
|
||||
.get();
|
||||
.where(eq(oauthCodes.code, code));
|
||||
|
||||
if (!record) return null;
|
||||
if (record.used !== 0) return null;
|
||||
@@ -89,17 +86,21 @@ export async function exchangeCode(
|
||||
if (computedChallenge !== record.codeChallenge) return null;
|
||||
|
||||
// Mark code as used
|
||||
db.update(oauthCodes).set({ used: 1 }).where(eq(oauthCodes.code, code)).run();
|
||||
await db
|
||||
.update(oauthCodes)
|
||||
.set({ used: 1 })
|
||||
.where(eq(oauthCodes.code, code));
|
||||
|
||||
return generateTokens(db, clientId);
|
||||
return generateTokens(db, clientId, userId);
|
||||
}
|
||||
|
||||
// ── Token Management ─────────────────────────────────────────────────
|
||||
|
||||
function generateTokens(
|
||||
async function generateTokens(
|
||||
db: Db,
|
||||
clientId: string,
|
||||
): { accessToken: string; refreshToken: string; expiresIn: number } {
|
||||
userId: number,
|
||||
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
|
||||
const accessToken = randomBytes(32).toString("hex");
|
||||
const refreshToken = randomBytes(32).toString("hex");
|
||||
|
||||
@@ -113,15 +114,14 @@ function generateTokens(
|
||||
const expiresAt = new Date(Date.now() + 3600 * 1000); // 1 hour
|
||||
const refreshExpiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
|
||||
|
||||
db.insert(oauthTokens)
|
||||
.values({
|
||||
accessTokenHash,
|
||||
refreshTokenHash,
|
||||
clientId,
|
||||
expiresAt,
|
||||
refreshExpiresAt,
|
||||
})
|
||||
.run();
|
||||
await db.insert(oauthTokens).values({
|
||||
accessTokenHash,
|
||||
refreshTokenHash,
|
||||
clientId,
|
||||
userId,
|
||||
expiresAt,
|
||||
refreshExpiresAt,
|
||||
});
|
||||
|
||||
return { accessToken, refreshToken, expiresIn: 3600 };
|
||||
}
|
||||
@@ -129,25 +129,25 @@ function generateTokens(
|
||||
export async function verifyAccessToken(
|
||||
db: Db = prodDb,
|
||||
token: string,
|
||||
): Promise<boolean> {
|
||||
): Promise<{ userId: number } | null> {
|
||||
const tokenHash = createHash("sha256").update(token).digest("hex");
|
||||
|
||||
const record = db
|
||||
const [record] = await db
|
||||
.select()
|
||||
.from(oauthTokens)
|
||||
.where(eq(oauthTokens.accessTokenHash, tokenHash))
|
||||
.get();
|
||||
.where(eq(oauthTokens.accessTokenHash, tokenHash));
|
||||
|
||||
if (!record) return false;
|
||||
if (record.expiresAt < new Date()) return false;
|
||||
if (!record) return null;
|
||||
if (record.expiresAt < new Date()) return null;
|
||||
|
||||
return true;
|
||||
return { userId: record.userId };
|
||||
}
|
||||
|
||||
export async function refreshAccessToken(
|
||||
db: Db = prodDb,
|
||||
refreshToken: string,
|
||||
clientId: string,
|
||||
userId: number,
|
||||
): Promise<{
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
@@ -155,7 +155,7 @@ export async function refreshAccessToken(
|
||||
} | null> {
|
||||
const tokenHash = createHash("sha256").update(refreshToken).digest("hex");
|
||||
|
||||
const record = db
|
||||
const [record] = await db
|
||||
.select()
|
||||
.from(oauthTokens)
|
||||
.where(
|
||||
@@ -163,22 +163,21 @@ export async function refreshAccessToken(
|
||||
eq(oauthTokens.refreshTokenHash, tokenHash),
|
||||
eq(oauthTokens.clientId, clientId),
|
||||
),
|
||||
)
|
||||
.get();
|
||||
);
|
||||
|
||||
if (!record) return null;
|
||||
if (record.refreshExpiresAt < new Date()) return null;
|
||||
|
||||
// Delete old token pair
|
||||
db.delete(oauthTokens).where(eq(oauthTokens.id, record.id)).run();
|
||||
await db.delete(oauthTokens).where(eq(oauthTokens.id, record.id));
|
||||
|
||||
return generateTokens(db, clientId);
|
||||
return generateTokens(db, clientId, userId);
|
||||
}
|
||||
|
||||
// ── Cleanup ──────────────────────────────────────────────────────────
|
||||
|
||||
export function cleanExpiredOAuthData(db: Db = prodDb): void {
|
||||
export async function cleanExpiredOAuthData(db: Db = prodDb): Promise<void> {
|
||||
const now = new Date();
|
||||
db.delete(oauthCodes).where(lt(oauthCodes.expiresAt, now)).run();
|
||||
db.delete(oauthTokens).where(lt(oauthTokens.expiresAt, now)).run();
|
||||
await db.delete(oauthCodes).where(lt(oauthCodes.expiresAt, now));
|
||||
await db.delete(oauthTokens).where(lt(oauthTokens.expiresAt, now));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user