142 lines
4.3 KiB
TypeScript
142 lines
4.3 KiB
TypeScript
import type { DbClient } from "@hivegrid/db/client.server";
|
|
import { syncDefaultRolesAndPermissions } from "@hivegrid/db/permissions-seed.server";
|
|
import { syncAdminRoleCatalog } from "@hivegrid/db/admin-rbac.server";
|
|
import argon2 from "argon2";
|
|
import {
|
|
assertLocalCredentialsDatabaseMarker,
|
|
assertLocalCredentialsEnvironment,
|
|
type LocalCredentialsEnvironment,
|
|
} from "./localCredentialsEnvironment.server";
|
|
|
|
export const LOCAL_ADMIN_EMAIL = "admin@email.com";
|
|
export const LOCAL_ADMIN_PASSWORD = "password";
|
|
|
|
export type LocalCredentialsClient = Pick<DbClient, "$queryRaw" | "$transaction">;
|
|
|
|
export type LocalCredentialsResult = {
|
|
adminUserId: string;
|
|
normalizedPasswordIdentityCount: number;
|
|
};
|
|
|
|
export async function ensureLocalCredentials({
|
|
client,
|
|
env = process.env,
|
|
hashPassword = hashLocalPassword,
|
|
syncPermissions = async () => {
|
|
await syncDefaultRolesAndPermissions({ client: client as DbClient });
|
|
},
|
|
syncAdminPermissions = async () => {
|
|
await syncAdminRoleCatalog(client as DbClient);
|
|
},
|
|
}: {
|
|
client: LocalCredentialsClient;
|
|
env?: LocalCredentialsEnvironment;
|
|
hashPassword?: (password: string) => Promise<string>;
|
|
syncPermissions?: () => Promise<void>;
|
|
syncAdminPermissions?: () => Promise<void>;
|
|
}): Promise<LocalCredentialsResult> {
|
|
assertLocalCredentialsEnvironment(env);
|
|
await assertLocalCredentialsDatabaseMarker(client);
|
|
const passwordHash = await hashPassword(LOCAL_ADMIN_PASSWORD);
|
|
await syncPermissions();
|
|
await syncAdminPermissions();
|
|
|
|
return client.$transaction(async (transaction) => {
|
|
const admin = await transaction.user.upsert({
|
|
where: { email: LOCAL_ADMIN_EMAIL },
|
|
create: {
|
|
email: LOCAL_ADMIN_EMAIL,
|
|
passwordHash,
|
|
firstName: "Admin",
|
|
lastName: "User",
|
|
avatar: "",
|
|
phone: "",
|
|
active: true,
|
|
},
|
|
update: {
|
|
passwordHash,
|
|
active: true,
|
|
pendingDeletion: false,
|
|
scheduledDeletionAt: null,
|
|
deletionRequestedAt: null,
|
|
pseudonymized: false,
|
|
pseudonymizedAt: null,
|
|
deletionRequestId: null,
|
|
totpSecretEncrypted: null,
|
|
totpPendingSecretEncrypted: null,
|
|
totpEnabledAt: null,
|
|
totpPendingCreatedAt: null,
|
|
},
|
|
select: { id: true },
|
|
});
|
|
|
|
await transaction.adminUser.updateMany({
|
|
where: { email: { not: LOCAL_ADMIN_EMAIL }, status: "active" },
|
|
data: {
|
|
status: "disabled",
|
|
disabledAt: new Date(),
|
|
disabledReason: "Local canonical admin normalization",
|
|
},
|
|
});
|
|
const adminIdentity = await transaction.adminUser.upsert({
|
|
where: { email: LOCAL_ADMIN_EMAIL },
|
|
create: {
|
|
id: admin.id,
|
|
email: LOCAL_ADMIN_EMAIL,
|
|
firstName: "Admin",
|
|
lastName: "User",
|
|
status: "active",
|
|
authentikGroups: [],
|
|
},
|
|
update: {
|
|
firstName: "Admin",
|
|
lastName: "User",
|
|
status: "active",
|
|
disabledAt: null,
|
|
disabledReason: null,
|
|
},
|
|
select: { id: true },
|
|
});
|
|
|
|
const adminRole = await transaction.adminRole.findUnique({
|
|
where: { systemKey: "super_admin" },
|
|
select: { id: true },
|
|
});
|
|
if (!adminRole) {
|
|
throw new Error("Local credential setup could not resolve the canonical SuperAdmin role.");
|
|
}
|
|
|
|
// Delete and recreate so this remains idempotent even after `prisma db push`,
|
|
// where migration-only partial unique indexes may not exist yet.
|
|
await transaction.adminUserRole.deleteMany({
|
|
where: { adminUserId: adminIdentity.id },
|
|
});
|
|
await transaction.adminUserRole.create({
|
|
data: { adminUserId: adminIdentity.id, adminRoleId: adminRole.id, grantedByAdminUserId: adminIdentity.id },
|
|
});
|
|
|
|
const normalizedUsers = await transaction.user.updateMany({
|
|
where: { passwordHash: { not: null } },
|
|
data: { passwordHash },
|
|
});
|
|
const normalizedPortalUsers = await transaction.portalUser.updateMany({
|
|
where: { passwordHash: { not: null } },
|
|
data: { passwordHash },
|
|
});
|
|
|
|
return {
|
|
adminUserId: adminIdentity.id,
|
|
normalizedPasswordIdentityCount: normalizedUsers.count + normalizedPortalUsers.count,
|
|
};
|
|
});
|
|
}
|
|
|
|
async function hashLocalPassword(password: string): Promise<string> {
|
|
return argon2.hash(password, {
|
|
type: argon2.argon2id,
|
|
memoryCost: 19_456,
|
|
timeCost: 2,
|
|
parallelism: 1,
|
|
});
|
|
}
|