134 lines
4.2 KiB
TypeScript
134 lines
4.2 KiB
TypeScript
import { LoginRateLimitedError, type AuthRepository } from "@minecraft-account-manager/auth";
|
|
import { and, desc, eq, gt, isNull, lt, sql } from "drizzle-orm";
|
|
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
import * as schema from "./schema";
|
|
import { loginCodes, sessions, users } from "./schema";
|
|
|
|
type Database = PostgresJsDatabase<typeof schema>;
|
|
|
|
export function createAuthRepository(db: Database): AuthRepository {
|
|
return {
|
|
async saveLoginCode(code) {
|
|
await db.transaction(async (tx) => {
|
|
await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${code.discordUserId}))`);
|
|
const [latestCode] = await tx
|
|
.select({ createdAt: loginCodes.createdAt })
|
|
.from(loginCodes)
|
|
.where(eq(loginCodes.discordUserId, code.discordUserId))
|
|
.orderBy(desc(loginCodes.createdAt))
|
|
.limit(1);
|
|
|
|
if (latestCode && latestCode.createdAt > new Date(code.createdAt.getTime() - 30_000)) {
|
|
throw new LoginRateLimitedError();
|
|
}
|
|
|
|
await tx.delete(loginCodes).where(lt(loginCodes.expiresAt, code.createdAt));
|
|
await tx
|
|
.update(loginCodes)
|
|
.set({ consumedAt: code.createdAt })
|
|
.where(
|
|
and(
|
|
eq(loginCodes.discordUserId, code.discordUserId),
|
|
isNull(loginCodes.consumedAt),
|
|
),
|
|
);
|
|
await tx.insert(loginCodes).values({
|
|
tokenHash: code.tokenHash,
|
|
discordUserId: code.discordUserId,
|
|
discordUsername: code.discordUsername,
|
|
discordGlobalName: code.discordGlobalName,
|
|
expiresAt: code.expiresAt,
|
|
createdAt: code.createdAt,
|
|
});
|
|
});
|
|
},
|
|
|
|
async exchangeLoginCode(input) {
|
|
return db.transaction(async (tx) => {
|
|
const [loginCode] = await tx
|
|
.update(loginCodes)
|
|
.set({ consumedAt: input.now })
|
|
.where(
|
|
and(
|
|
eq(loginCodes.tokenHash, input.loginCodeHash),
|
|
isNull(loginCodes.consumedAt),
|
|
gt(loginCodes.expiresAt, input.now),
|
|
),
|
|
)
|
|
.returning();
|
|
|
|
if (!loginCode) {
|
|
return null;
|
|
}
|
|
|
|
const [existingUser] = await tx
|
|
.select({ id: users.id })
|
|
.from(users)
|
|
.where(eq(users.discordUserId, loginCode.discordUserId))
|
|
.limit(1);
|
|
|
|
const [user] = await tx
|
|
.insert(users)
|
|
.values({
|
|
discordUserId: loginCode.discordUserId,
|
|
discordUsername: loginCode.discordUsername,
|
|
discordGlobalName: loginCode.discordGlobalName,
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: users.discordUserId,
|
|
set: {
|
|
discordUsername: loginCode.discordUsername,
|
|
discordGlobalName: loginCode.discordGlobalName,
|
|
updatedAt: input.now,
|
|
},
|
|
})
|
|
.returning({
|
|
id: users.id,
|
|
discordUserId: users.discordUserId,
|
|
discordUsername: users.discordUsername,
|
|
firstName: users.firstName,
|
|
});
|
|
|
|
if (!user) {
|
|
throw new Error("Failed to create or update the Discord user");
|
|
}
|
|
|
|
await tx.insert(sessions).values({
|
|
userId: user.id,
|
|
tokenHash: input.sessionTokenHash,
|
|
expiresAt: input.sessionExpiresAt,
|
|
lastSeenAt: input.now,
|
|
createdAt: input.now,
|
|
});
|
|
|
|
return { user, isNewUser: !existingUser };
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function findUserBySessionToken(db: Database, tokenHash: string, now = new Date()) {
|
|
const [user] = await db
|
|
.select({
|
|
id: users.id,
|
|
discordUserId: users.discordUserId,
|
|
discordUsername: users.discordUsername,
|
|
discordGlobalName: users.discordGlobalName,
|
|
firstName: users.firstName,
|
|
onboardingCompletedAt: users.onboardingCompletedAt,
|
|
sessionExpiresAt: sessions.expiresAt,
|
|
})
|
|
.from(sessions)
|
|
.innerJoin(users, eq(sessions.userId, users.id))
|
|
.where(
|
|
and(
|
|
eq(sessions.tokenHash, tokenHash),
|
|
isNull(sessions.revokedAt),
|
|
gt(sessions.expiresAt, now),
|
|
),
|
|
)
|
|
.limit(1);
|
|
|
|
return user ?? null;
|
|
}
|