mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-10-03 17:43:13 +00:00
92 lines
2 KiB
TypeScript
92 lines
2 KiB
TypeScript
|
|
import { eq } from "drizzle-orm";
|
||
|
|
import type { Db } from "../db/client.js";
|
||
|
|
import { users } from "../db/schema/index.js";
|
||
|
|
import type { UserRole } from "../env.js";
|
||
|
|
|
||
|
|
export type AuthIdentity = {
|
||
|
|
cognitoSub: string;
|
||
|
|
email: string;
|
||
|
|
name: string;
|
||
|
|
role: UserRole;
|
||
|
|
};
|
||
|
|
|
||
|
|
export type AuthUser = {
|
||
|
|
id: string;
|
||
|
|
cognitoSub: string;
|
||
|
|
email: string;
|
||
|
|
name: string;
|
||
|
|
role: UserRole;
|
||
|
|
};
|
||
|
|
|
||
|
|
export class IdentityConflictError extends Error {
|
||
|
|
readonly status = 409 as const;
|
||
|
|
|
||
|
|
constructor(readonly email: string) {
|
||
|
|
super(`Email ${email} is already linked to a different identity.`);
|
||
|
|
this.name = "IdentityConflictError";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function toAuthUser(row: {
|
||
|
|
id: string;
|
||
|
|
cognitoSub: string;
|
||
|
|
email: string;
|
||
|
|
name: string;
|
||
|
|
role: UserRole;
|
||
|
|
}): AuthUser {
|
||
|
|
return {
|
||
|
|
id: row.id,
|
||
|
|
cognitoSub: row.cognitoSub,
|
||
|
|
email: row.email,
|
||
|
|
name: row.name,
|
||
|
|
role: row.role,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Upsert by Cognito subject only. Never rebind an existing email to a new
|
||
|
|
* subject — that would allow account takeover if email claims collide.
|
||
|
|
*/
|
||
|
|
export async function upsertUserFromIdentity(
|
||
|
|
handle: Db,
|
||
|
|
identity: AuthIdentity,
|
||
|
|
): Promise<AuthUser> {
|
||
|
|
const bySub = await handle.db.query.users.findFirst({
|
||
|
|
where: eq(users.cognitoSub, identity.cognitoSub),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (bySub) {
|
||
|
|
const [updated] = await handle.db
|
||
|
|
.update(users)
|
||
|
|
.set({
|
||
|
|
email: identity.email,
|
||
|
|
name: identity.name,
|
||
|
|
role: identity.role,
|
||
|
|
updatedAt: new Date(),
|
||
|
|
})
|
||
|
|
.where(eq(users.id, bySub.id))
|
||
|
|
.returning();
|
||
|
|
return toAuthUser(updated);
|
||
|
|
}
|
||
|
|
|
||
|
|
const byEmail = await handle.db.query.users.findFirst({
|
||
|
|
where: eq(users.email, identity.email),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (byEmail) {
|
||
|
|
throw new IdentityConflictError(identity.email);
|
||
|
|
}
|
||
|
|
|
||
|
|
const [created] = await handle.db
|
||
|
|
.insert(users)
|
||
|
|
.values({
|
||
|
|
cognitoSub: identity.cognitoSub,
|
||
|
|
email: identity.email,
|
||
|
|
name: identity.name,
|
||
|
|
role: identity.role,
|
||
|
|
})
|
||
|
|
.returning();
|
||
|
|
|
||
|
|
return toAuthUser(created);
|
||
|
|
}
|