wip
This commit is contained in:
parent
36b17d73eb
commit
ce8e56fe29
16 changed files with 693 additions and 72 deletions
|
|
@ -28,8 +28,10 @@ const Alert = React.forwardRef<
|
|||
Alert.displayName = "Alert";
|
||||
|
||||
const AlertTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props}>{props.children}</h5>
|
||||
({ className, children, ...props }, ref) => (
|
||||
<h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props}>
|
||||
{children}
|
||||
</h5>
|
||||
),
|
||||
);
|
||||
AlertTitle.displayName = "AlertTitle";
|
||||
|
|
|
|||
|
|
@ -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<typeof auth>(), usernameClient(), twoFactorClient()],
|
||||
plugins: [
|
||||
inferAdditionalFields<typeof auth>(),
|
||||
usernameClient(),
|
||||
adminClient(),
|
||||
organizationClient(),
|
||||
twoFactorClient(),
|
||||
ssoClient(),
|
||||
],
|
||||
});
|
||||
|
|
|
|||
36
app/client/modules/auth/routes/auth-error.tsx
Normal file
36
app/client/modules/auth/routes/auth-error.tsx
Normal file
|
|
@ -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 (
|
||||
<AuthLayout title="Authentication Error" description="An error occurred during authentication">
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{formattedError}</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button>
|
||||
<Link to="/login">Back to Login</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<AuthLayout title="Two-Factor Authentication" description="Enter the 6-digit code from your authenticator app">
|
||||
|
|
@ -240,6 +255,26 @@ export default function LoginPage() {
|
|||
</form>
|
||||
</Form>
|
||||
|
||||
{providers && providers.length > 0 && (
|
||||
<>
|
||||
<div className="relative my-4">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
{providers.map((p) => (
|
||||
<Button key={p.id} variant="outline" className="w-full" onClick={() => handleSSOLogin(p.providerId)}>
|
||||
Sign in with {p.providerId}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ResetPasswordDialog open={showResetDialog} onOpenChange={setShowResetDialog} />
|
||||
</AuthLayout>
|
||||
);
|
||||
|
|
|
|||
237
app/client/modules/settings/components/sso-section.tsx
Normal file
237
app/client/modules/settings/components/sso-section.tsx
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<div className="border-t border-border/50 bg-card-header p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield className="size-5" />
|
||||
SSO Providers
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-1.5">Manage Single Sign-On providers for your instance</CardDescription>
|
||||
</div>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<Plus className="size-4 mr-2" />
|
||||
Add Provider
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<form onSubmit={handleRegister}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add SSO Provider</DialogTitle>
|
||||
<DialogDescription>Enter the details of your OIDC provider.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="providerId">Provider ID</Label>
|
||||
<Input
|
||||
id="providerId"
|
||||
value={providerId}
|
||||
onChange={(e) => setProviderId(e.target.value)}
|
||||
placeholder="e.g. google, okta-work"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="issuer">Issuer URL</Label>
|
||||
<Input
|
||||
id="issuer"
|
||||
value={issuer}
|
||||
onChange={(e) => setIssuer(e.target.value)}
|
||||
placeholder="https://accounts.google.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="domain">Domain</Label>
|
||||
<Input
|
||||
id="domain"
|
||||
value={domain}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
placeholder="example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="clientId">Client ID</Label>
|
||||
<Input id="clientId" value={clientId} onChange={(e) => setClientId(e.target.value)} required />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="clientSecret">Client Secret</Label>
|
||||
<Input
|
||||
id="clientSecret"
|
||||
type="password"
|
||||
value={clientSecret}
|
||||
onChange={(e) => setClientSecret(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2 mt-2">
|
||||
<Label className="text-xs text-muted-foreground uppercase">Callback URL</Label>
|
||||
<div className="p-2 bg-muted rounded text-xs break-all font-mono">{callbackUrl}</div>
|
||||
<p className="text-[10px] text-muted-foreground italic">
|
||||
Copy this URL to your Identity Provider configuration.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" loading={isRegistering}>
|
||||
Register Provider
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
<CardContent className="p-6">
|
||||
{providers.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">No SSO providers configured.</p>
|
||||
) : (
|
||||
<div className="border rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Provider ID</TableHead>
|
||||
<TableHead>Domain</TableHead>
|
||||
<TableHead>User id</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{providers.map((p) => {
|
||||
const linkedAccount = userAccounts.find((ua) => ua.providerId === p.providerId);
|
||||
|
||||
return (
|
||||
<TableRow key={p.id}>
|
||||
<TableCell className="font-medium">{p.providerId}</TableCell>
|
||||
<TableCell>{p.domain}</TableCell>
|
||||
<TableCell>{linkedAccount ? linkedAccount.userId : "-"}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{linkedAccount ? (
|
||||
<Button variant="destructive" size="sm" onClick={() => handleUnlink(p.providerId)}>
|
||||
{<Link2 className="size-4 mr-2" />}
|
||||
Unlink your account
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="ghost" size="sm" onClick={() => handleLink(p.providerId)}>
|
||||
<LinkIcon className="size-4 mr-2" />
|
||||
Link your account
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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) {
|
|||
<CardContent className="p-6 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Username</Label>
|
||||
<Input value={loaderData.user?.username || ""} disabled className="max-w-md" />
|
||||
<Input value={user?.username} disabled className="max-w-md" />
|
||||
</div>
|
||||
{/* <div className="space-y-2"> */}
|
||||
{/* <Label>Email</Label> */}
|
||||
|
|
@ -278,8 +284,8 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContent>
|
||||
|
||||
<TwoFactorSection twoFactorEnabled={loaderData.user?.twoFactorEnabled} />
|
||||
<TwoFactorSection twoFactorEnabled={user?.twoFactorEnabled} />
|
||||
<SSOSection providers={loaderData.ssoProviders} userAccounts={loaderData.userAccounts} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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<MiddlewareOptions, AuthContext<BetterAuthOptions>>;
|
||||
|
|
@ -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<typeof createBetterAuth>;
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
}),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ export const requireAuth = createMiddleware(async (c, next) => {
|
|||
return c.json<unknown>({ message: "Invalid or expired session" }, 401);
|
||||
}
|
||||
|
||||
if (user.role !== "admin") {
|
||||
// For now we only have admin users
|
||||
return c.json<unknown>({ message: "Insufficient permissions" }, 403);
|
||||
}
|
||||
|
||||
c.set("user", user);
|
||||
|
||||
await next();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
148
auth-schema.ts
Normal file
148
auth-schema.ts
Normal file
|
|
@ -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],
|
||||
}),
|
||||
}));
|
||||
43
bun.lock
43
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=="],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue