From ce8e56fe29de4654647c60babaae1f99c9eeb025 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sat, 17 Jan 2026 12:33:16 +0100 Subject: [PATCH] wip --- app/client/components/ui/alert.tsx | 6 +- app/client/lib/auth-client.ts | 13 +- app/client/modules/auth/routes/auth-error.tsx | 36 +++ app/client/modules/auth/routes/login.tsx | 37 ++- .../settings/components/sso-section.tsx | 237 ++++++++++++++++++ .../modules/settings/routes/settings.tsx | 16 +- .../auth-middlewares/convert-legacy-user.ts | 1 + app/lib/auth.ts | 30 ++- app/routes.ts | 1 + app/server/db/schema.ts | 140 +++++++---- app/server/modules/auth/auth.middleware.ts | 5 + .../modules/system/system.controller.ts | 15 +- app/server/modules/system/system.dto.ts | 36 +++ auth-schema.ts | 148 +++++++++++ bun.lock | 43 +++- package.json | 1 + 16 files changed, 693 insertions(+), 72 deletions(-) create mode 100644 app/client/modules/auth/routes/auth-error.tsx create mode 100644 app/client/modules/settings/components/sso-section.tsx create mode 100644 auth-schema.ts diff --git a/app/client/components/ui/alert.tsx b/app/client/components/ui/alert.tsx index 229491a7..e5aab076 100644 --- a/app/client/components/ui/alert.tsx +++ b/app/client/components/ui/alert.tsx @@ -28,8 +28,10 @@ const Alert = React.forwardRef< Alert.displayName = "Alert"; const AlertTitle = React.forwardRef>( - ({ className, ...props }, ref) => ( -
{props.children}
+ ({ className, children, ...props }, ref) => ( +
+ {children} +
), ); AlertTitle.displayName = "AlertTitle"; diff --git a/app/client/lib/auth-client.ts b/app/client/lib/auth-client.ts index 8dfd12a5..85f24c69 100644 --- a/app/client/lib/auth-client.ts +++ b/app/client/lib/auth-client.ts @@ -1,8 +1,17 @@ import { createAuthClient } from "better-auth/react"; -import { twoFactorClient, usernameClient } from "better-auth/client/plugins"; +import { organizationClient, twoFactorClient, usernameClient } from "better-auth/client/plugins"; import { inferAdditionalFields } from "better-auth/client/plugins"; +import { ssoClient } from "@better-auth/sso/client"; +import { adminClient } from "better-auth/client/plugins"; import type { auth } from "~/lib/auth"; export const authClient = createAuthClient({ - plugins: [inferAdditionalFields(), usernameClient(), twoFactorClient()], + plugins: [ + inferAdditionalFields(), + usernameClient(), + adminClient(), + organizationClient(), + twoFactorClient(), + ssoClient(), + ], }); diff --git a/app/client/modules/auth/routes/auth-error.tsx b/app/client/modules/auth/routes/auth-error.tsx new file mode 100644 index 00000000..bb325a6c --- /dev/null +++ b/app/client/modules/auth/routes/auth-error.tsx @@ -0,0 +1,36 @@ +import { AlertCircle } from "lucide-react"; +import { Link, useSearchParams } from "react-router"; +import { AuthLayout } from "~/client/components/auth-layout"; +import { Button } from "~/client/components/ui/button"; +import { Alert, AlertDescription, AlertTitle } from "~/client/components/ui/alert"; +import type { Route } from "./+types/auth-error"; + +export function meta(_: Route.MetaArgs) { + return [{ title: "Authentication Error - Zerobyte" }]; +} + +export default function AuthErrorPage() { + const [searchParams] = useSearchParams(); + const errorMessage = searchParams.get("error") || "An unknown error occurred"; + + const formattedError = errorMessage + .split(/[_-]/) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + + return ( + + + + Error + {formattedError} + + +
+ +
+
+ ); +} diff --git a/app/client/modules/auth/routes/login.tsx b/app/client/modules/auth/routes/login.tsx index 555ce544..2d0db4f9 100644 --- a/app/client/modules/auth/routes/login.tsx +++ b/app/client/modules/auth/routes/login.tsx @@ -14,6 +14,7 @@ import { authClient } from "~/client/lib/auth-client"; import { authMiddleware } from "~/middleware/auth"; import { ResetPasswordDialog } from "../components/reset-password-dialog"; import type { Route } from "./+types/login"; +import { listSsoProviders } from "~/client/api-client"; export const clientMiddleware = [authMiddleware]; @@ -34,7 +35,14 @@ const loginSchema = type({ type LoginFormValues = typeof loginSchema.inferIn; -export default function LoginPage() { +export async function clientLoader(_: Route.ClientLoaderArgs) { + const providers = await listSsoProviders(); + + return { providers: providers.data ?? [] }; +} + +export default function LoginPage({ loaderData }: Route.ComponentProps) { + const { providers } = loaderData; const navigate = useNavigate(); const [showResetDialog, setShowResetDialog] = useState(false); const [isLoggingIn, setIsLoggingIn] = useState(false); @@ -128,6 +136,13 @@ export default function LoginPage() { form.reset(); }; + const handleSSOLogin = async (providerId: string) => { + await authClient.signIn.sso({ + providerId, + callbackURL: "/volumes", + }); + }; + if (requires2FA) { return ( @@ -240,6 +255,26 @@ export default function LoginPage() { + {providers && providers.length > 0 && ( + <> +
+
+ +
+
+ Or continue with +
+
+
+ {providers.map((p) => ( + + ))} +
+ + )} +
); diff --git a/app/client/modules/settings/components/sso-section.tsx b/app/client/modules/settings/components/sso-section.tsx new file mode 100644 index 00000000..b0c36d08 --- /dev/null +++ b/app/client/modules/settings/components/sso-section.tsx @@ -0,0 +1,237 @@ +import { Plus, Shield, Link as LinkIcon, Link2 } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { Button } from "~/client/components/ui/button"; +import { CardContent, CardDescription, CardTitle } from "~/client/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "~/client/components/ui/dialog"; +import { Input } from "~/client/components/ui/input"; +import { Label } from "~/client/components/ui/label"; +import { authClient } from "~/client/lib/auth-client"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table"; + +interface SSOProvider { + id: string; + providerId: string; + issuer: string; + domain: string; +} + +interface SSOSectionProps { + providers: SSOProvider[]; + userAccounts: { providerId: string; userId: string }[]; +} + +export function SSOSection({ providers, userAccounts }: SSOSectionProps) { + const [isRegistering, setIsRegistering] = useState(false); + const [open, setOpen] = useState(false); + + const [providerId, setProviderId] = useState(""); + const [issuer, setIssuer] = useState(""); + const [domain, setDomain] = useState(""); + const [clientId, setClientId] = useState(""); + const [clientSecret, setClientSecret] = useState(""); + + const handleRegister = async (e: React.FormEvent) => { + e.preventDefault(); + + await authClient.sso.register({ + providerId, + issuer, + domain, + oidcConfig: { + clientId, + clientSecret, + }, + fetchOptions: { + onRequest: () => { + setIsRegistering(true); + }, + onResponse: () => { + setIsRegistering(false); + }, + onError: async ({ error }) => { + toast.error("Failed to register SSO provider", { description: error.message }); + }, + onSuccess: async () => { + toast.success("SSO provider registered successfully"); + window.location.reload(); + }, + }, + }); + }; + + const callbackUrl = `${window.location.origin}/api/auth/sso/callback/${providerId || ":providerId"}`; + + const handleLink = async (providerId: string) => { + await authClient.signIn.sso({ + providerId, + callbackURL: window.location.href, + fetchOptions: { + onError: async ({ error }) => { + toast.error("Failed to link SSO provider", { description: error.message }); + }, + onSuccess: async () => { + toast.success("SSO provider linked successfully"); + window.location.reload(); + }, + }, + }); + }; + + const handleUnlink = async (providerId: string) => { + await authClient.unlinkAccount({ + providerId, + fetchOptions: { + onError: async ({ error }) => { + toast.error("Failed to unlink SSO provider", { description: error.message }); + }, + onSuccess: async () => { + toast.success("SSO provider unlinked successfully"); + window.location.reload(); + }, + }, + }); + }; + + return ( + <> +
+
+
+ + + SSO Providers + + Manage Single Sign-On providers for your instance +
+ + + + + +
+ + Add SSO Provider + Enter the details of your OIDC provider. + +
+
+ + setProviderId(e.target.value)} + placeholder="e.g. google, okta-work" + required + /> +
+
+ + setIssuer(e.target.value)} + placeholder="https://accounts.google.com" + required + /> +
+
+ + setDomain(e.target.value)} + placeholder="example.com" + required + /> +
+
+ + setClientId(e.target.value)} required /> +
+
+ + setClientSecret(e.target.value)} + required + /> +
+
+ +
{callbackUrl}
+

+ Copy this URL to your Identity Provider configuration. +

+
+
+ + + +
+
+
+
+
+ + {providers.length === 0 ? ( +

No SSO providers configured.

+ ) : ( +
+ + + + Provider ID + Domain + User id + Actions + + + + {providers.map((p) => { + const linkedAccount = userAccounts.find((ua) => ua.providerId === p.providerId); + + return ( + + {p.providerId} + {p.domain} + {linkedAccount ? linkedAccount.userId : "-"} + + {linkedAccount ? ( + + ) : ( + + )} + + + ); + })} + +
+
+ )} +
+ + ); +} diff --git a/app/client/modules/settings/routes/settings.tsx b/app/client/modules/settings/routes/settings.tsx index f06054da..de5811cc 100644 --- a/app/client/modules/settings/routes/settings.tsx +++ b/app/client/modules/settings/routes/settings.tsx @@ -20,7 +20,9 @@ import { Label } from "~/client/components/ui/label"; import { authClient } from "~/client/lib/auth-client"; import { appContext } from "~/context"; import { TwoFactorSection } from "../components/two-factor-section"; +import { SSOSection } from "../components/sso-section"; import type { Route } from "./+types/settings"; +import { listSsoProviders } from "~/client/api-client"; export const handle = { breadcrumb: () => [{ label: "Settings" }], @@ -36,12 +38,16 @@ export function meta(_: Route.MetaArgs) { ]; } -export async function clientLoader({ context }: Route.LoaderArgs) { +export async function clientLoader({ context }: Route.ClientLoaderArgs) { const ctx = context.get(appContext); - return ctx; + const providers = await listSsoProviders(); + const accounts = await authClient.listAccounts(); + + return { ...ctx, ssoProviders: providers.data ?? [], userAccounts: accounts.data ?? [] }; } export default function Settings({ loaderData }: Route.ComponentProps) { + const { user } = loaderData; const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); @@ -155,7 +161,7 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
- +
{/*
*/} {/* */} @@ -278,8 +284,8 @@ export default function Settings({ loaderData }: Route.ComponentProps) { - - + + ); } diff --git a/app/lib/auth-middlewares/convert-legacy-user.ts b/app/lib/auth-middlewares/convert-legacy-user.ts index f4ce3b35..652fc7b1 100644 --- a/app/lib/auth-middlewares/convert-legacy-user.ts +++ b/app/lib/auth-middlewares/convert-legacy-user.ts @@ -32,6 +32,7 @@ export const convertLegacyUserOnFirstLogin = async (ctx: AuthMiddlewareContext) name: legacyUser.name, hasDownloadedResticPassword: legacyUser.hasDownloadedResticPassword, emailVerified: false, + role: "admin", }); await tx.insert(account).values({ diff --git a/app/lib/auth.ts b/app/lib/auth.ts index 3e4b3feb..8cc5941d 100644 --- a/app/lib/auth.ts +++ b/app/lib/auth.ts @@ -6,11 +6,11 @@ import { type MiddlewareOptions, } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; -import { createAuthMiddleware, twoFactor, username } from "better-auth/plugins"; +import { createAuthMiddleware, twoFactor, username, admin, organization } from "better-auth/plugins"; +import { sso } from "@better-auth/sso"; import { convertLegacyUserOnFirstLogin } from "./auth-middlewares/convert-legacy-user"; import { cryptoUtils } from "~/server/utils/crypto"; import { db } from "~/server/db/db"; -import { ensureOnlyOneUser } from "./auth-middlewares/only-one-user"; import { config } from "~/server/core/config"; export type AuthMiddlewareContext = MiddlewareContext>; @@ -21,10 +21,25 @@ const createBetterAuth = (secret: string) => trustedOrigins: config.trustedOrigins ?? ["*"], hooks: { before: createAuthMiddleware(async (ctx) => { - await ensureOnlyOneUser(ctx); await convertLegacyUserOnFirstLogin(ctx); }), }, + databaseHooks: { + user: { + create: { + before: async (user) => { + const anyUser = await db.query.usersTable.findFirst(); + const isFirstUser = !anyUser; + + if (isFirstUser) { + user.role = "admin"; + } + + return { data: user }; + }, + }, + }, + }, database: drizzleAdapter(db, { provider: "sqlite", }), @@ -50,13 +65,22 @@ const createBetterAuth = (secret: string) => }, plugins: [ username(), + admin({ defaultRole: "user" }), + organization(), twoFactor({ backupCodeOptions: { storeBackupCodes: "encrypted", amount: 5, }, }), + sso(), ], + account: { + accountLinking: { + enabled: true, + allowDifferentEmails: true, + }, + }, }); type Auth = ReturnType; diff --git a/app/routes.ts b/app/routes.ts index 3eb58c1c..67db3bd7 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -4,6 +4,7 @@ export default [ route("onboarding", "./client/modules/auth/routes/onboarding.tsx"), route("login", "./client/modules/auth/routes/login.tsx"), route("download-recovery-key", "./client/modules/auth/routes/download-recovery-key.tsx"), + route("auth/error", "./client/modules/auth/routes/auth-error.tsx"), layout("./client/components/layout.tsx", [ route("/", "./client/routes/root.tsx"), route("volumes", "./client/modules/volumes/routes/volumes.tsx"), diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts index b5fc5b6d..9ccac2dd 100644 --- a/app/server/db/schema.ts +++ b/app/server/db/schema.ts @@ -57,6 +57,10 @@ export const usersTable = sqliteTable("users_table", { image: text("image"), displayUsername: text("display_username"), twoFactorEnabled: integer("two_factor_enabled", { mode: "boolean" }).notNull().default(false), + role: text("role"), + banned: integer("banned", { mode: "boolean" }).default(false), + banReason: text("ban_reason"), + banExpires: integer("ban_expires", { mode: "timestamp_ms" }), }); export type User = typeof usersTable.$inferSelect; @@ -78,6 +82,7 @@ export const sessionsTable = sqliteTable( .default(sql`(unixepoch() * 1000)`), ipAddress: text("ip_address"), userAgent: text("user_agent"), + impersonatedBy: text("impersonated_by"), }, (table) => [index("sessionsTable_userId_idx").on(table.userId)], ); @@ -132,26 +137,6 @@ export const verification = sqliteTable( (table) => [index("verification_identifier_idx").on(table.identifier)], ); -export const userRelations = relations(usersTable, ({ many }) => ({ - sessions: many(sessionsTable), - accounts: many(account), - twoFactors: many(twoFactor), -})); - -export const sessionRelations = relations(sessionsTable, ({ one }) => ({ - user: one(usersTable, { - fields: [sessionsTable.userId], - references: [usersTable.id], - }), -})); - -export const accountRelations = relations(account, ({ one }) => ({ - user: one(usersTable, { - fields: [account.userId], - references: [usersTable.id], - }), -})); - /** * Repositories Table */ @@ -223,18 +208,6 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", { }); export type BackupScheduleInsert = typeof backupSchedulesTable.$inferInsert; -export const backupScheduleRelations = relations(backupSchedulesTable, ({ one, many }) => ({ - volume: one(volumesTable, { - fields: [backupSchedulesTable.volumeId], - references: [volumesTable.id], - }), - repository: one(repositoriesTable, { - fields: [backupSchedulesTable.repositoryId], - references: [repositoriesTable.id], - }), - notifications: many(backupScheduleNotificationsTable), - mirrors: many(backupScheduleMirrorsTable), -})); export type BackupSchedule = typeof backupSchedulesTable.$inferSelect; /** @@ -253,9 +226,6 @@ export const notificationDestinationsTable = sqliteTable("notification_destinati .notNull() .default(sql`(unixepoch() * 1000)`), }); -export const notificationDestinationRelations = relations(notificationDestinationsTable, ({ many }) => ({ - schedules: many(backupScheduleNotificationsTable), -})); export type NotificationDestination = typeof notificationDestinationsTable.$inferSelect; /** @@ -280,16 +250,6 @@ export const backupScheduleNotificationsTable = sqliteTable( }, (table) => [primaryKey({ columns: [table.scheduleId, table.destinationId] })], ); -export const backupScheduleNotificationRelations = relations(backupScheduleNotificationsTable, ({ one }) => ({ - schedule: one(backupSchedulesTable, { - fields: [backupScheduleNotificationsTable.scheduleId], - references: [backupSchedulesTable.id], - }), - destination: one(notificationDestinationsTable, { - fields: [backupScheduleNotificationsTable.destinationId], - references: [notificationDestinationsTable.id], - }), -})); export type BackupScheduleNotification = typeof backupScheduleNotificationsTable.$inferSelect; /** @@ -317,16 +277,6 @@ export const backupScheduleMirrorsTable = sqliteTable( (table) => [unique().on(table.scheduleId, table.repositoryId)], ); -export const backupScheduleMirrorRelations = relations(backupScheduleMirrorsTable, ({ one }) => ({ - schedule: one(backupSchedulesTable, { - fields: [backupScheduleMirrorsTable.scheduleId], - references: [backupSchedulesTable.id], - }), - repository: one(repositoriesTable, { - fields: [backupScheduleMirrorsTable.repositoryId], - references: [repositoriesTable.id], - }), -})); export type BackupScheduleMirror = typeof backupScheduleMirrorsTable.$inferSelect; /** @@ -357,3 +307,83 @@ export const twoFactor = sqliteTable( }, (table) => [index("twoFactor_secret_idx").on(table.secret), index("twoFactor_userId_idx").on(table.userId)], ); + +export const ssoProvider = sqliteTable("sso_provider", { + id: text("id").primaryKey(), + issuer: text("issuer").notNull(), + oidcConfig: text("oidc_config"), + samlConfig: text("saml_config"), + userId: text("user_id").references(() => usersTable.id, { + onDelete: "cascade", + }), + providerId: text("provider_id").notNull().unique(), + organizationId: text("organization_id"), + domain: text("domain").notNull(), +}); + +export const notificationDestinationRelations = relations(notificationDestinationsTable, ({ many }) => ({ + schedules: many(backupScheduleNotificationsTable), +})); + +export const backupScheduleNotificationRelations = relations(backupScheduleNotificationsTable, ({ one }) => ({ + schedule: one(backupSchedulesTable, { + fields: [backupScheduleNotificationsTable.scheduleId], + references: [backupSchedulesTable.id], + }), + destination: one(notificationDestinationsTable, { + fields: [backupScheduleNotificationsTable.destinationId], + references: [notificationDestinationsTable.id], + }), +})); + +export const backupScheduleMirrorRelations = relations(backupScheduleMirrorsTable, ({ one }) => ({ + schedule: one(backupSchedulesTable, { + fields: [backupScheduleMirrorsTable.scheduleId], + references: [backupSchedulesTable.id], + }), + repository: one(repositoriesTable, { + fields: [backupScheduleMirrorsTable.repositoryId], + references: [repositoriesTable.id], + }), +})); + +export const backupScheduleRelations = relations(backupSchedulesTable, ({ one, many }) => ({ + volume: one(volumesTable, { + fields: [backupSchedulesTable.volumeId], + references: [volumesTable.id], + }), + repository: one(repositoriesTable, { + fields: [backupSchedulesTable.repositoryId], + references: [repositoriesTable.id], + }), + notifications: many(backupScheduleNotificationsTable), + mirrors: many(backupScheduleMirrorsTable), +})); + +export const userRelations = relations(usersTable, ({ many }) => ({ + sessions: many(sessionsTable), + accounts: many(account), + twoFactors: many(twoFactor), + ssoProviders: many(ssoProvider), +})); + +export const sessionRelations = relations(sessionsTable, ({ one }) => ({ + user: one(usersTable, { + fields: [sessionsTable.userId], + references: [usersTable.id], + }), +})); + +export const accountRelations = relations(account, ({ one }) => ({ + user: one(usersTable, { + fields: [account.userId], + references: [usersTable.id], + }), +})); + +export const ssoProviderRelations = relations(ssoProvider, ({ one }) => ({ + usersTable: one(usersTable, { + fields: [ssoProvider.userId], + references: [usersTable.id], + }), +})); diff --git a/app/server/modules/auth/auth.middleware.ts b/app/server/modules/auth/auth.middleware.ts index a9a211c0..a31c48bd 100644 --- a/app/server/modules/auth/auth.middleware.ts +++ b/app/server/modules/auth/auth.middleware.ts @@ -26,6 +26,11 @@ export const requireAuth = createMiddleware(async (c, next) => { return c.json({ message: "Invalid or expired session" }, 401); } + if (user.role !== "admin") { + // For now we only have admin users + return c.json({ message: "Insufficient permissions" }, 403); + } + c.set("user", user); await next(); diff --git a/app/server/modules/system/system.controller.ts b/app/server/modules/system/system.controller.ts index 326fe33f..1245758a 100644 --- a/app/server/modules/system/system.controller.ts +++ b/app/server/modules/system/system.controller.ts @@ -1,9 +1,11 @@ import { Hono } from "hono"; import { validator } from "hono-openapi"; import { + deleteSSOProviderDto, downloadResticPasswordBodySchema, downloadResticPasswordDto, getUpdatesDto, + listSSOProvidersDto, systemInfoDto, type SystemInfoDto, type UpdateInfoDto, @@ -12,11 +14,15 @@ import { systemService } from "./system.service"; import { requireAuth } from "../auth/auth.middleware"; import { RESTIC_PASS_FILE } from "../../core/constants"; import { db } from "../../db/db"; -import { usersTable } from "../../db/schema"; +import { ssoProvider, usersTable } from "../../db/schema"; import { eq } from "drizzle-orm"; import { verifyUserPassword } from "../auth/helpers"; export const systemController = new Hono() + .get("/sso-providers", listSSOProvidersDto, async (c) => { + const providers = await db.select().from(ssoProvider); + return c.json(providers, 200); + }) .use(requireAuth) .get("/info", systemInfoDto, async (c) => { const info = await systemService.getSystemInfo(); @@ -55,4 +61,9 @@ export const systemController = new Hono() return c.json({ message: "Failed to read Restic password file" }, 500); } }, - ); + ) + .delete("/sso-providers/:id", deleteSSOProviderDto, async (c) => { + const id = c.req.param("id"); + await db.delete(ssoProvider).where(eq(ssoProvider.id, id)); + return c.json({ message: "Provider deleted" }, 200); + }); diff --git a/app/server/modules/system/system.dto.ts b/app/server/modules/system/system.dto.ts index 3eccafd8..09f0fd81 100644 --- a/app/server/modules/system/system.dto.ts +++ b/app/server/modules/system/system.dto.ts @@ -79,3 +79,39 @@ export const downloadResticPasswordDto = describeRoute({ }, }, }); + +export const ssoProviderSchema = type({ + id: "string", + providerId: "string", + issuer: "string", + domain: "string", +}); + +export const listSSOProvidersResponse = ssoProviderSchema.array(); + +export const listSSOProvidersDto = describeRoute({ + description: "List all configured SSO providers", + tags: ["Auth"], + operationId: "listSSOProviders", + responses: { + 200: { + description: "List of SSO providers", + content: { + "application/json": { + schema: resolver(listSSOProvidersResponse), + }, + }, + }, + }, +}); + +export const deleteSSOProviderDto = describeRoute({ + description: "Delete an SSO provider", + tags: ["Auth"], + operationId: "deleteSSOProvider", + responses: { + 200: { + description: "Provider deleted", + }, + }, +}); diff --git a/auth-schema.ts b/auth-schema.ts new file mode 100644 index 00000000..901a5912 --- /dev/null +++ b/auth-schema.ts @@ -0,0 +1,148 @@ +import { relations } from "drizzle-orm"; +import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"; + +export const usersTable = sqliteTable("users_table", { + id: text("id").primaryKey(), + name: text("name").notNull(), + email: text("email").notNull().unique(), + emailVerified: integer("email_verified", { mode: "boolean" }).default(false).notNull(), + image: text("image"), + createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .$onUpdate(() => new Date()) + .notNull(), + username: text("username").notNull(), + displayUsername: text("display_username"), + role: text("role"), + banned: integer("banned", { mode: "boolean" }).default(false), + banReason: text("ban_reason"), + banExpires: integer("ban_expires", { mode: "timestamp_ms" }), + twoFactorEnabled: integer("two_factor_enabled", { mode: "boolean" }).default(false), + hasDownloadedResticPassword: integer("has_downloaded_restic_password", { + mode: "boolean", + }), +}); + +export const sessionsTable = sqliteTable( + "sessions_table", + { + id: text("id").primaryKey(), + expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), + token: text("token").notNull().unique(), + createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .$onUpdate(() => new Date()) + .notNull(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + userId: text("user_id") + .notNull() + .references(() => usersTable.id, { onDelete: "cascade" }), + impersonatedBy: text("impersonated_by"), + }, + (table) => [index("sessionsTable_userId_idx").on(table.userId)], +); + +export const account = sqliteTable( + "account", + { + id: text("id").primaryKey(), + accountId: text("account_id").notNull(), + providerId: text("provider_id").notNull(), + userId: text("user_id") + .notNull() + .references(() => usersTable.id, { onDelete: "cascade" }), + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + idToken: text("id_token"), + accessTokenExpiresAt: integer("access_token_expires_at", { + mode: "timestamp_ms", + }), + refreshTokenExpiresAt: integer("refresh_token_expires_at", { + mode: "timestamp_ms", + }), + scope: text("scope"), + password: text("password"), + createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .$onUpdate(() => new Date()) + .notNull(), + }, + (table) => [index("account_userId_idx").on(table.userId)], +); + +export const verification = sqliteTable( + "verification", + { + id: text("id").primaryKey(), + identifier: text("identifier").notNull(), + value: text("value").notNull(), + expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), + createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .$onUpdate(() => new Date()) + .notNull(), + }, + (table) => [index("verification_identifier_idx").on(table.identifier)], +); + +export const twoFactor = sqliteTable( + "two_factor", + { + id: text("id").primaryKey(), + secret: text("secret").notNull(), + backupCodes: text("backup_codes").notNull(), + userId: text("user_id") + .notNull() + .references(() => usersTable.id, { onDelete: "cascade" }), + }, + (table) => [index("twoFactor_secret_idx").on(table.secret), index("twoFactor_userId_idx").on(table.userId)], +); + +export const ssoProvider = sqliteTable("sso_provider", { + id: text("id").primaryKey(), + issuer: text("issuer").notNull(), + oidcConfig: text("oidc_config"), + samlConfig: text("saml_config"), + userId: text("user_id").references(() => usersTable.id, { + onDelete: "cascade", + }), + providerId: text("provider_id").notNull().unique(), + organizationId: text("organization_id"), + domain: text("domain").notNull(), +}); + +export const usersTableRelations = relations(usersTable, ({ many }) => ({ + sessionsTables: many(sessionsTable), + accounts: many(account), + twoFactors: many(twoFactor), + ssoProviders: many(ssoProvider), +})); + +export const sessionsTableRelations = relations(sessionsTable, ({ one }) => ({ + usersTable: one(usersTable, { + fields: [sessionsTable.userId], + references: [usersTable.id], + }), +})); + +export const accountRelations = relations(account, ({ one }) => ({ + usersTable: one(usersTable, { + fields: [account.userId], + references: [usersTable.id], + }), +})); + +export const twoFactorRelations = relations(twoFactor, ({ one }) => ({ + usersTable: one(usersTable, { + fields: [twoFactor.userId], + references: [usersTable.id], + }), +})); + +export const ssoProviderRelations = relations(ssoProvider, ({ one }) => ({ + usersTable: one(usersTable, { + fields: [ssoProvider.userId], + references: [usersTable.id], + }), +})); diff --git a/bun.lock b/bun.lock index 47e97b37..53346a45 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "zerobyte", "dependencies": { + "@better-auth/sso": "^1.4.10", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -108,6 +109,8 @@ "@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="], + "@authenio/xml-encryption": ["@authenio/xml-encryption@2.0.2", "", { "dependencies": { "@xmldom/xmldom": "^0.8.6", "escape-html": "^1.0.3", "xpath": "0.0.32" } }, "sha512-cTlrKttbrRHEw3W+0/I609A2Matj5JQaRvfLtEIGZvlN0RaPi+3ANsMeqAyCAVlH/lUIW2tmtBlSMni74lcXeg=="], + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], @@ -168,7 +171,9 @@ "@better-auth/core": ["@better-auth/core@1.4.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-VfqZwMAEl9rnGx092BIZ2Q5z8rt7jjN2OAbvPqehufSKZGmh8JsdtZRBMl/CHQir9bwi2Ev0UF4+7TQp+DXEMg=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.4.12", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.12" } }, "sha512-4q504Og42PzkUbZjXDt+FyeYaS0WZmAlEOC3nbBCZDObTVCRUnGgJW52B2maJ7BCVvAQgBGLEeQmQzU5+63J0A=="], + "@better-auth/sso": ["@better-auth/sso@1.4.10", "", { "dependencies": { "@better-fetch/fetch": "1.1.21", "fast-xml-parser": "^5.2.5", "jose": "^6.1.0", "samlify": "^2.10.1", "zod": "^4.1.12" }, "peerDependencies": { "better-auth": "1.4.10" } }, "sha512-td8Mg32JHpyFRIwJ6sfqZ0NDa9Easf+sXw5wnWLLgmnd7/osg4xTKTFsMLEvr5j4n/1mzSFVU/RBthOV2lCD+A=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.4.10", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.10" } }, "sha512-Dq4XJX6EKsUu0h3jpRagX739p/VMOTcnJYWRrLtDYkqtZFg+sFiFsSWVcfapZoWpRSUGYX9iKwl6nDHn6Ju2oQ=="], "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], @@ -636,7 +641,9 @@ "@types/node": ["@types/node@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="], - "@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="], + "@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="], + + "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], @@ -654,6 +661,10 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "@xmldom/is-dom-node": ["@xmldom/is-dom-node@1.0.1", "", {}, "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q=="], + + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="], + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], @@ -676,6 +687,8 @@ "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.11", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-mwq3W3e/pKSI6TG8lXMiDWvEi1VXYlSBlJlB3l+I0bAb5u1RNUl88udos85eOPNK3m5EXK9uO7d2g08pesTySQ=="], @@ -724,6 +737,8 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + "caniuse-lite": ["caniuse-lite@1.0.30001761", "", {}, "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -918,6 +933,8 @@ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fast-xml-parser": ["fast-xml-parser@5.3.3", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-2O3dkPAAC6JavuMm8+4+pgTk+5hoAs+CjZ+sWcQLkX9+/tHRuTkQh/Oaifr8qDmZ8iEHb771Ea6G8CdwkrgvYA=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], @@ -1226,8 +1243,12 @@ "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + "node-forge": ["node-forge@1.3.3", "", {}, "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg=="], + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + "node-rsa": ["node-rsa@1.1.1", "", { "dependencies": { "asn1": "^0.2.4" } }, "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw=="], + "nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -1254,6 +1275,8 @@ "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], @@ -1392,6 +1415,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "samlify": ["samlify@2.10.2", "", { "dependencies": { "@authenio/xml-encryption": "^2.0.2", "@xmldom/xmldom": "^0.8.6", "camelcase": "^6.2.0", "node-forge": "^1.3.0", "node-rsa": "^1.1.1", "pako": "^1.0.10", "uuid": "^8.3.2", "xml": "^1.0.1", "xml-crypto": "^6.1.2", "xml-escape": "^1.1.0", "xpath": "^0.0.32" } }, "sha512-y5s1cHwclqwP8h7K2Wj9SfP1q+1S9+jrs5OAegYTLAiuFi7nDvuKqbiXLmUTvYPMpzHcX94wTY2+D604jgTKvA=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -1450,6 +1475,8 @@ "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], @@ -1530,6 +1557,8 @@ "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], @@ -1570,6 +1599,14 @@ "wsl-utils": ["wsl-utils@0.3.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-3sFIGLiaDP7rTO4xh3g+b3AzhYDIUGGywE/WsmqzJWDxus5aJXVnPTNC/6L+r2WzrwXqVOdD262OaO+cEyPMSQ=="], + "xml": ["xml@1.0.1", "", {}, "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="], + + "xml-crypto": ["xml-crypto@6.1.2", "", { "dependencies": { "@xmldom/is-dom-node": "^1.0.1", "@xmldom/xmldom": "^0.8.10", "xpath": "^0.0.33" } }, "sha512-leBOVQdVi8FvPJrMYoum7Ici9qyxfE4kVi+AkpUoYCSXaQF4IlBm1cneTK9oAxR61LpYxTx7lNcsnBIeRpGW2w=="], + + "xml-escape": ["xml-escape@1.1.0", "", {}, "sha512-B/T4sDK8Z6aUh/qNr7mjKAwwncIljFuUP+DO/D5hloYFj+90O88z8Wf7oSucZTHxBAsC1/CTP4rtx/x1Uf72Mg=="], + + "xpath": ["xpath@0.0.32", "", {}, "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], @@ -1690,6 +1727,8 @@ "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "xml-crypto/xpath": ["xpath@0.0.33", "", {}, "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA=="], + "@happy-dom/global-registrator/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], diff --git a/package.json b/package.json index 58569c85..0bf3deab 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "test:codegen": "playwright codegen localhost:4096" }, "dependencies": { + "@better-auth/sso": "^1.4.10", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2",