@flaredev/cli 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/index.js +340 -163
  2. package/package.json +3 -2
  3. package/templates/app/app/api/openapi.json/route.ts +23 -0
  4. package/templates/app/app/api/reference/route.ts +36 -0
  5. package/templates/app/app/dashboard/account/page.tsx +38 -0
  6. package/templates/app/app/dashboard/layout.tsx +39 -0
  7. package/templates/app/app/dashboard/page.tsx +42 -9
  8. package/templates/app/app/forgot-password/page.tsx +22 -0
  9. package/templates/app/app/globals.css +109 -4
  10. package/templates/app/app/layout.tsx +28 -26
  11. package/templates/app/app/page.tsx +126 -13
  12. package/templates/app/app/reset-password/page.tsx +31 -0
  13. package/templates/app/app/sign-in/page.tsx +49 -33
  14. package/templates/app/app/sign-up/page.tsx +42 -23
  15. package/templates/app/app/two-factor/page.tsx +25 -0
  16. package/templates/app/components/account/security-settings.tsx +525 -0
  17. package/templates/app/components/admin/fields/country-field.tsx +152 -0
  18. package/templates/app/components/admin/fields/field-widget.tsx +186 -128
  19. package/templates/app/components/admin/fields/multi-select-field.tsx +46 -0
  20. package/templates/app/components/admin/fields/phone-field.tsx +86 -0
  21. package/templates/app/components/admin/resource-nav.tsx +120 -109
  22. package/templates/app/components/admin/resource-table.tsx +31 -1
  23. package/templates/app/components/auth/auth-shell.tsx +284 -0
  24. package/templates/app/components/auth/password-forms.tsx +79 -0
  25. package/templates/app/components/auth/provider-icons.tsx +39 -0
  26. package/templates/app/components/auth/sign-in-flow.tsx +315 -0
  27. package/templates/app/components/auth/sign-up-form.tsx +101 -0
  28. package/templates/app/components/auth/social-buttons.tsx +56 -0
  29. package/templates/app/components/auth/two-factor-form.tsx +124 -0
  30. package/templates/app/components/auth/ui.tsx +96 -0
  31. package/templates/app/components/dashboard/dashboard-nav.tsx +35 -0
  32. package/templates/app/components/marketing/product-preview.tsx +70 -0
  33. package/templates/app/components/marketing/site-header.tsx +39 -0
  34. package/templates/app/components/sign-out-button.tsx +1 -1
  35. package/templates/app/components/ui/checkbox.tsx +25 -0
  36. package/templates/app/components/ui/radio-group.tsx +29 -0
  37. package/templates/app/db/auth-schema.ts +167 -118
  38. package/templates/app/lib/admin-nav.ts +1 -1
  39. package/templates/app/lib/api-docs.ts +16 -0
  40. package/templates/app/lib/auth-client.ts +16 -1
  41. package/templates/app/lib/auth-config.ts +33 -0
  42. package/templates/app/lib/auth-emails.ts +94 -0
  43. package/templates/app/lib/auth.ts +81 -6
  44. package/templates/app/lib/site.ts +47 -0
  45. package/templates/app/lib/theme.ts +79 -0
  46. package/templates/app/migrations/0000_init.sql +32 -1
  47. package/templates/app/migrations/meta/0000_snapshot.json +215 -1
  48. package/templates/app/migrations/meta/_journal.json +1 -1
  49. package/templates/app/vite.config.ts +5 -3
  50. package/templates/app/components/auth-form.tsx +0 -120
@@ -0,0 +1,33 @@
1
+ /**
2
+ * How people sign in to this app. `flare create` wrote the choices you made; change
3
+ * them here any time. Every method's tables already exist, so switching one on or off
4
+ * needs no migration, and a method that's off is refused by the API, not just hidden.
5
+ *
6
+ * Email and password sign-in is always available.
7
+ */
8
+ export const authConfig = {
9
+ /** A sign-in link by email. */
10
+ magicLink: true,
11
+ /** A 6-digit sign-in code by email. */
12
+ emailOtp: true,
13
+ /** Face ID, Touch ID, Windows Hello or a security key. */
14
+ passkeys: true,
15
+ twoFactor: {
16
+ /** Codes from an authenticator app (TOTP), with backup codes. */
17
+ authenticator: true,
18
+ /** Codes by email as the second step. */
19
+ email: true,
20
+ },
21
+ /**
22
+ * Social sign-in. Each provider also needs its credentials (see .dev.vars.example);
23
+ * without them its button stays hidden.
24
+ */
25
+ social: [] as SocialProvider[],
26
+ /** Refuse password sign-in until the email address is verified. */
27
+ requireEmailVerification: false,
28
+ };
29
+
30
+ export type SocialProvider = "google" | "github" | "apple" | "microsoft";
31
+
32
+ /** Whether any second factor can be set up. */
33
+ export const twoFactorAvailable = authConfig.twoFactor.authenticator || authConfig.twoFactor.email;
@@ -0,0 +1,94 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { getDb } from "@/db";
3
+ import { user } from "@/db/auth-schema";
4
+ import { sendTransactionalEmail } from "./mail";
5
+
6
+ /**
7
+ * Every email the sign-in flows send. Change the wording here; the layout comes from
8
+ * lib/mail.ts. Without RESEND_API_KEY these print to the console, links and codes included.
9
+ */
10
+
11
+ const ignore = "If you didn't ask for this, you can ignore this email.";
12
+
13
+ /** Accounts with two-factor sign-in never get a one-step email sign-in (it would skip the second factor). */
14
+ async function usesTwoFactor(email: string): Promise<boolean> {
15
+ const row = await getDb().select({ on: user.twoFactorEnabled }).from(user).where(eq(user.email, email.toLowerCase())).get();
16
+ return Boolean(row?.on);
17
+ }
18
+
19
+ async function sendTwoFactorNotice(email: string) {
20
+ await sendTransactionalEmail({
21
+ to: email,
22
+ subject: "Sign in with your password or a passkey",
23
+ heading: "Email sign-in is off for your account",
24
+ paragraphs: [
25
+ "Someone asked to sign in to your account by email. Your account uses two-factor authentication, so we don't send sign-in links or codes.",
26
+ "Sign in with your password and second factor, or with a passkey.",
27
+ ],
28
+ footer: ignore,
29
+ });
30
+ }
31
+
32
+ export async function sendMagicLink({ email, url }: { email: string; url: string }) {
33
+ if (await usesTwoFactor(email)) return sendTwoFactorNotice(email);
34
+ await sendTransactionalEmail({
35
+ to: email,
36
+ subject: "Your sign-in link",
37
+ heading: "Sign in",
38
+ paragraphs: ["Use this link to sign in. It works once and expires in 5 minutes."],
39
+ action: { label: "Sign in", url },
40
+ footer: ignore,
41
+ preheader: "Your sign-in link",
42
+ });
43
+ }
44
+
45
+ export async function sendEmailCode({ email, otp, type }: { email: string; otp: string; type: "sign-in" | "email-verification" | "forget-password" | "change-email" }) {
46
+ if (type === "sign-in" && (await usesTwoFactor(email))) return sendTwoFactorNotice(email);
47
+ const purpose = {
48
+ "sign-in": ["Your sign-in code", "Enter this code to sign in."],
49
+ "email-verification": ["Verify your email", "Enter this code to verify your email address."],
50
+ "forget-password": ["Reset your password", "Enter this code to choose a new password."],
51
+ "change-email": ["Confirm your new email", "Enter this code to confirm your new email address."],
52
+ }[type];
53
+ await sendTransactionalEmail({
54
+ to: email,
55
+ subject: `${purpose[0]}: ${otp}`,
56
+ heading: otp,
57
+ paragraphs: [purpose[1], "It expires in 5 minutes."],
58
+ footer: ignore,
59
+ preheader: purpose[0],
60
+ });
61
+ }
62
+
63
+ export async function sendTwoFactorCode({ user, otp }: { user: { email: string }; otp: string }) {
64
+ await sendTransactionalEmail({
65
+ to: user.email,
66
+ subject: `Your verification code: ${otp}`,
67
+ heading: otp,
68
+ paragraphs: ["Enter this code to finish signing in. It expires in 5 minutes."],
69
+ footer: "If you didn't just sign in, change your password: someone else knows it.",
70
+ preheader: "Your verification code",
71
+ });
72
+ }
73
+
74
+ export async function sendPasswordReset({ user, url }: { user: { email: string }; url: string }) {
75
+ await sendTransactionalEmail({
76
+ to: user.email,
77
+ subject: "Reset your password",
78
+ heading: "Reset your password",
79
+ paragraphs: ["Use this link to choose a new password. It expires in 1 hour."],
80
+ action: { label: "Choose a new password", url },
81
+ footer: ignore,
82
+ });
83
+ }
84
+
85
+ export async function sendVerificationEmail({ user, url }: { user: { email: string }; url: string }) {
86
+ await sendTransactionalEmail({
87
+ to: user.email,
88
+ subject: "Verify your email",
89
+ heading: "Verify your email",
90
+ paragraphs: ["Confirm this is your email address."],
91
+ action: { label: "Verify email", url },
92
+ footer: ignore,
93
+ });
94
+ }
@@ -1,13 +1,55 @@
1
1
  import { env, waitUntil } from "cloudflare:workers";
2
- import { betterAuth } from "better-auth";
2
+ import { passkey } from "@better-auth/passkey";
3
3
  import { drizzleAdapter } from "@better-auth/drizzle-adapter";
4
+ import { betterAuth } from "better-auth";
5
+ import { APIError, createAuthMiddleware } from "better-auth/api";
4
6
  import { nextCookies } from "better-auth/next-js";
5
7
  import { admin } from "better-auth/plugins/admin";
8
+ import { emailOTP } from "better-auth/plugins/email-otp";
9
+ import { magicLink } from "better-auth/plugins/magic-link";
10
+ import { twoFactor } from "better-auth/plugins/two-factor";
6
11
  import { hashPassword, verifyPassword } from "@flaredev/core";
7
12
  import { getDb, schema } from "@/db";
13
+ import { authConfig, type SocialProvider } from "./auth-config";
14
+ import { sendEmailCode, sendMagicLink, sendPasswordReset, sendTwoFactorCode, sendVerificationEmail } from "./auth-emails";
15
+
16
+ const vars = env as unknown as Record<string, string | undefined>;
8
17
 
9
18
  /** Optional: set when serving from a custom domain (e.g. https://example.com). */
10
- const configuredURL = (env as { BETTER_AUTH_URL?: string }).BETTER_AUTH_URL;
19
+ const configuredURL = vars.BETTER_AUTH_URL;
20
+
21
+ /** A provider's credentials from the environment, when both are set. */
22
+ function credentials(prefix: string) {
23
+ const clientId = vars[`${prefix}_CLIENT_ID`];
24
+ const clientSecret = vars[`${prefix}_CLIENT_SECRET`];
25
+ return clientId && clientSecret ? { clientId, clientSecret } : undefined;
26
+ }
27
+
28
+ const providerSettings: Record<SocialProvider, Record<string, string> | undefined> = {
29
+ google: credentials("GOOGLE"),
30
+ github: credentials("GITHUB"),
31
+ // Apple's client secret is a JWT you generate from your key (see the auth guide).
32
+ apple: credentials("APPLE") && { ...credentials("APPLE")!, ...(vars.APPLE_APP_BUNDLE_ID ? { appBundleIdentifier: vars.APPLE_APP_BUNDLE_ID } : {}) },
33
+ // "common" accepts personal and work accounts; set a tenant id to allow only your organisation.
34
+ microsoft: credentials("MICROSOFT") && { ...credentials("MICROSOFT")!, tenantId: vars.MICROSOFT_TENANT_ID || "common" },
35
+ };
36
+
37
+ /** Social providers switched on in lib/auth-config.ts that also have credentials. */
38
+ const socialProviders = Object.fromEntries(
39
+ authConfig.social.flatMap((provider) => (providerSettings[provider] ? [[provider, providerSettings[provider]]] : [])),
40
+ );
41
+
42
+ /** Endpoints of methods switched off in lib/auth-config.ts: refused, not just hidden. */
43
+ function disabledPath(path: string): boolean {
44
+ if (!authConfig.magicLink && (path === "/sign-in/magic-link" || path === "/magic-link/verify")) return true;
45
+ if (!authConfig.emailOtp && (path.startsWith("/email-otp/") || path === "/sign-in/email-otp" || path === "/forget-password/email-otp")) return true;
46
+ if (!authConfig.passkeys && path.startsWith("/passkey/")) return true;
47
+ const { authenticator, email } = authConfig.twoFactor;
48
+ if (!authenticator && !email && path.startsWith("/two-factor/")) return true;
49
+ if (!authenticator && (path === "/two-factor/get-totp-uri" || path === "/two-factor/verify-totp")) return true;
50
+ if (!email && (path === "/two-factor/send-otp" || path === "/two-factor/verify-otp")) return true;
51
+ return false;
52
+ }
11
53
 
12
54
  export const auth = betterAuth({
13
55
  appName: "__APP_NAME__",
@@ -18,20 +60,53 @@ export const auth = betterAuth({
18
60
  baseURL: configuredURL || {
19
61
  allowedHosts: ["localhost:*", "127.0.0.1:*", "*.trycloudflare.com", "__APP_NAME__.*.workers.dev"],
20
62
  },
63
+ // Sign in with Apple posts back from Apple's own origin.
64
+ trustedOrigins: authConfig.social.includes("apple") ? ["https://appleid.apple.com"] : [],
21
65
  database: drizzleAdapter(getDb(), { provider: "sqlite", schema }),
22
- socialProviders: __SOCIAL_PROVIDERS__,
66
+ socialProviders,
67
+ account: {
68
+ // Signing in with Google as ada@example.com joins Ada's existing account (the provider has verified the address).
69
+ accountLinking: { enabled: true, trustedProviders: ["google", "github", "apple", "microsoft"] },
70
+ },
23
71
  emailAndPassword: {
24
72
  enabled: true,
73
+ requireEmailVerification: authConfig.requireEmailVerification,
25
74
  // PBKDF2 via WebCrypto: the default scrypt exceeds Workers CPU limits.
26
75
  password: { hash: hashPassword, verify: verifyPassword },
76
+ sendResetPassword: ({ user, url }) => sendPasswordReset({ user, url }),
77
+ resetPasswordTokenExpiresIn: 60 * 60,
78
+ // A password reset signs every other device out.
79
+ revokeSessionsOnPasswordReset: true,
80
+ },
81
+ emailVerification: {
82
+ sendVerificationEmail: ({ user, url }) => sendVerificationEmail({ user, url }),
83
+ sendOnSignUp: true,
84
+ autoSignInAfterVerification: true,
27
85
  },
28
86
  advanced: {
29
87
  // Let emails and other deferred work finish after the response is sent.
30
88
  backgroundTasks: { handler: waitUntil },
31
89
  },
90
+ hooks: {
91
+ before: createAuthMiddleware(async (ctx) => {
92
+ if (disabledPath(ctx.path)) throw new APIError("NOT_FOUND", { message: "This sign-in method isn't enabled." });
93
+ }),
94
+ },
32
95
  plugins: [
33
96
  // Adds user.role (default "user") and admin APIs. The /admin area allows ADMIN_ROLES (lib/admin.ts).
34
97
  admin({ defaultRole: "user", adminRoles: ["admin"] }),
98
+ magicLink({ sendMagicLink: ({ email, url }) => sendMagicLink({ email, url }), expiresIn: 5 * 60, disableSignUp: false }),
99
+ emailOTP({ sendVerificationOTP: (data) => sendEmailCode(data), otpLength: 6, expiresIn: 5 * 60, allowedAttempts: 5 }),
100
+ twoFactor({
101
+ issuer: "__APP_NAME__",
102
+ otpOptions: { sendOTP: ({ user, otp }) => sendTwoFactorCode({ user, otp }), allowedAttempts: 5 },
103
+ // With only email codes switched on there's no authenticator app to confirm first.
104
+ skipVerificationOnEnable: !authConfig.twoFactor.authenticator,
105
+ // People who signed up with a social account have no password to confirm with;
106
+ // accounts that do have one must still give it.
107
+ allowPasswordless: true,
108
+ }),
109
+ passkey({ rpName: "__APP_NAME__" }),
35
110
  // Must stay last: lets server actions set auth cookies.
36
111
  nextCookies(),
37
112
  ],
@@ -39,7 +114,7 @@ export const auth = betterAuth({
39
114
 
40
115
  export type Session = typeof auth.$Infer.Session;
41
116
 
42
- /** OAuth providers with credentials configured, e.g. ["github"]. */
43
- export function enabledSocialProviders(): string[] {
44
- return Object.keys(auth.options.socialProviders ?? {});
117
+ /** OAuth providers switched on and configured, e.g. ["github"], for the sign-in buttons. */
118
+ export function enabledSocialProviders(): SocialProvider[] {
119
+ return Object.keys(socialProviders) as SocialProvider[];
45
120
  }
@@ -0,0 +1,47 @@
1
+ import type { ThemeName } from "./theme";
2
+
3
+ /**
4
+ * What the site says about itself: the home page, the sign-in screens' side panels and
5
+ * the page titles read from here. Edit freely.
6
+ */
7
+ export const site = {
8
+ name: "__APP_NAME__",
9
+ tagline: "The fastest way to run your business",
10
+ description: "__APP_NAME__ keeps your team, your customers and your data in one place, and it's live on the edge in every region.",
11
+ /**
12
+ * The look of the whole app: sign-in screens, dashboard, admin and home page.
13
+ * One of: default, coral, amber, sky, mono, emerald. FLARE_THEME (shell or .env)
14
+ * overrides it when the app is built or run with `flare dev`.
15
+ */
16
+ theme: "__THEME__" as ThemeName,
17
+ /** The small pill above the home page headline; set to null to hide it. */
18
+ announcement: { label: "New", text: "Passkeys and two-factor sign-in are here", href: "/sign-up" } as { label: string; text: string; href: string } | null,
19
+ /** The home page's feature grid (icon names from lucide.dev). */
20
+ features: [
21
+ { icon: "zap", title: "Fast everywhere", description: "Served from the edge in every region, so pages load in milliseconds wherever your customers are." },
22
+ { icon: "shield-check", title: "Secure by default", description: "Passkeys, two-factor sign-in, roles and rate limits, switched on from day one." },
23
+ { icon: "layout-dashboard", title: "One place for your data", description: "Every record your team keeps, with search, filters and an admin built in." },
24
+ { icon: "users", title: "Built for teams", description: "Invite your team, give each person the right access, and see who changed what." },
25
+ { icon: "plug", title: "Connects to your tools", description: "A documented API for every record, ready for your scripts and integrations." },
26
+ { icon: "credit-card", title: "Billing when you need it", description: "Plans, checkout and invoices through Stripe, without building a billing system." },
27
+ ],
28
+ /** Placeholder names for the logo strip: replace them with your customers (or remove the strip). */
29
+ customers: ["Acme", "Globex", "Initech", "Vandelay", "Northwind", "Brightline"],
30
+ /** Shown beside the sign-up form. */
31
+ highlights: [
32
+ "Set up in minutes, with nothing to install",
33
+ "Secure sign-in with passkeys and two-factor",
34
+ "Your data stays yours: export it any time",
35
+ ],
36
+ /** Shown on the sign-in screen of themes with a quote panel. */
37
+ testimonial: {
38
+ quote: "We moved our whole workflow over in an afternoon. The team hasn't looked back.",
39
+ author: "Alex Rivera",
40
+ role: "Head of Operations",
41
+ },
42
+ links: {
43
+ terms: "/terms",
44
+ privacy: "/privacy",
45
+ support: "mailto:support@example.com",
46
+ },
47
+ };
@@ -0,0 +1,79 @@
1
+ import { site } from "./site";
2
+
3
+ /**
4
+ * The six looks every Flare app ships with. The active one sets `data-theme` on <html>;
5
+ * app/globals.css maps it to colours, radius and type, and the auth screens pick the
6
+ * layout below.
7
+ */
8
+ export const THEMES = {
9
+ default: {
10
+ label: "Default",
11
+ description: "Calm and centred, indigo accents.",
12
+ auth: "centered",
13
+ social: "after",
14
+ socialStyle: "full",
15
+ signIn: (name: string) => ({ title: `Sign in to ${name}` }),
16
+ signUp: (name: string) => ({ title: `Create your ${name} account` }),
17
+ },
18
+ coral: {
19
+ label: "Coral",
20
+ description: "Warm and rounded, a card floating over the page.",
21
+ auth: "modal",
22
+ social: "after",
23
+ socialStyle: "icon",
24
+ signIn: () => ({ title: "Log in or sign up" }),
25
+ signUp: () => ({ title: "Create your account" }),
26
+ },
27
+ amber: {
28
+ label: "Amber",
29
+ description: "Plain and direct, a boxed form with pill buttons.",
30
+ auth: "boxed",
31
+ social: "after",
32
+ socialStyle: "full",
33
+ signIn: () => ({ title: "Sign in" }),
34
+ signUp: () => ({ title: "Create account" }),
35
+ },
36
+ sky: {
37
+ label: "Sky",
38
+ description: "Crisp blue, a bold heading with social sign-in first.",
39
+ auth: "banner",
40
+ social: "before",
41
+ socialStyle: "full",
42
+ signIn: (name: string) => ({ title: `Log in to ${name}` }),
43
+ signUp: (name: string) => ({ title: `Get started with ${name}` }),
44
+ },
45
+ mono: {
46
+ label: "Mono",
47
+ description: "Black and white on a fine grid, a showcase beside the form.",
48
+ auth: "showcase",
49
+ social: "after",
50
+ socialStyle: "full",
51
+ signIn: (name: string) => ({ title: `Log in to your ${name} account` }),
52
+ signUp: (name: string) => ({ title: `Create your ${name} account` }),
53
+ },
54
+ emerald: {
55
+ label: "Emerald",
56
+ description: "Fresh green, the form beside a customer quote.",
57
+ auth: "quote",
58
+ social: "before",
59
+ socialStyle: "full",
60
+ signIn: () => ({ title: "Welcome back", subtitle: "Sign in to your account" }),
61
+ signUp: () => ({ title: "Get started", subtitle: "Create a new account" }),
62
+ },
63
+ } as const;
64
+
65
+ export type ThemeName = keyof typeof THEMES;
66
+ export type AuthLayout = (typeof THEMES)[ThemeName]["auth"];
67
+
68
+ /** Set from FLARE_THEME by vite.config.ts when the app is built or run. */
69
+ declare const __FLARE_THEME__: string;
70
+
71
+ export function isThemeName(value: unknown): value is ThemeName {
72
+ return typeof value === "string" && value in THEMES;
73
+ }
74
+
75
+ /** The theme in use: FLARE_THEME if it names one, else lib/site.ts. */
76
+ export const activeTheme: ThemeName = isThemeName(__FLARE_THEME__) ? __FLARE_THEME__ : isThemeName(site.theme) ? site.theme : "default";
77
+
78
+ export const theme = THEMES[activeTheme];
79
+ export const authLayout: AuthLayout = theme.auth;
@@ -16,6 +16,23 @@ CREATE TABLE `account` (
16
16
  );
17
17
  --> statement-breakpoint
18
18
  CREATE INDEX `account_userId_idx` ON `account` (`user_id`);--> statement-breakpoint
19
+ CREATE TABLE `passkey` (
20
+ `id` text PRIMARY KEY NOT NULL,
21
+ `name` text,
22
+ `public_key` text NOT NULL,
23
+ `user_id` text NOT NULL,
24
+ `credential_id` text NOT NULL,
25
+ `counter` integer NOT NULL,
26
+ `device_type` text NOT NULL,
27
+ `backed_up` integer NOT NULL,
28
+ `transports` text,
29
+ `created_at` integer,
30
+ `aaguid` text,
31
+ FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
32
+ );
33
+ --> statement-breakpoint
34
+ CREATE INDEX `passkey_userId_idx` ON `passkey` (`user_id`);--> statement-breakpoint
35
+ CREATE INDEX `passkey_credentialID_idx` ON `passkey` (`credential_id`);--> statement-breakpoint
19
36
  CREATE TABLE `session` (
20
37
  `id` text PRIMARY KEY NOT NULL,
21
38
  `expires_at` integer NOT NULL,
@@ -31,6 +48,19 @@ CREATE TABLE `session` (
31
48
  --> statement-breakpoint
32
49
  CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint
33
50
  CREATE INDEX `session_userId_idx` ON `session` (`user_id`);--> statement-breakpoint
51
+ CREATE TABLE `two_factor` (
52
+ `id` text PRIMARY KEY NOT NULL,
53
+ `secret` text NOT NULL,
54
+ `backup_codes` text NOT NULL,
55
+ `user_id` text NOT NULL,
56
+ `verified` integer DEFAULT true,
57
+ `failed_verification_count` integer DEFAULT 0,
58
+ `locked_until` integer,
59
+ FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
60
+ );
61
+ --> statement-breakpoint
62
+ CREATE INDEX `twoFactor_secret_idx` ON `two_factor` (`secret`);--> statement-breakpoint
63
+ CREATE INDEX `twoFactor_userId_idx` ON `two_factor` (`user_id`);--> statement-breakpoint
34
64
  CREATE TABLE `user` (
35
65
  `id` text PRIMARY KEY NOT NULL,
36
66
  `name` text NOT NULL,
@@ -42,7 +72,8 @@ CREATE TABLE `user` (
42
72
  `role` text,
43
73
  `banned` integer DEFAULT false,
44
74
  `ban_reason` text,
45
- `ban_expires` integer
75
+ `ban_expires` integer,
76
+ `two_factor_enabled` integer DEFAULT false
46
77
  );
47
78
  --> statement-breakpoint
48
79
  CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "6",
3
3
  "dialect": "sqlite",
4
- "id": "0d286de4-e9a3-4fb7-a0d3-e3fa1f8ac57f",
4
+ "id": "79194ad1-2287-43f7-8f37-64fded59352b",
5
5
  "prevId": "00000000-0000-0000-0000-000000000000",
6
6
  "tables": {
7
7
  "account": {
@@ -128,6 +128,122 @@
128
128
  "uniqueConstraints": {},
129
129
  "checkConstraints": {}
130
130
  },
131
+ "passkey": {
132
+ "name": "passkey",
133
+ "columns": {
134
+ "id": {
135
+ "name": "id",
136
+ "type": "text",
137
+ "primaryKey": true,
138
+ "notNull": true,
139
+ "autoincrement": false
140
+ },
141
+ "name": {
142
+ "name": "name",
143
+ "type": "text",
144
+ "primaryKey": false,
145
+ "notNull": false,
146
+ "autoincrement": false
147
+ },
148
+ "public_key": {
149
+ "name": "public_key",
150
+ "type": "text",
151
+ "primaryKey": false,
152
+ "notNull": true,
153
+ "autoincrement": false
154
+ },
155
+ "user_id": {
156
+ "name": "user_id",
157
+ "type": "text",
158
+ "primaryKey": false,
159
+ "notNull": true,
160
+ "autoincrement": false
161
+ },
162
+ "credential_id": {
163
+ "name": "credential_id",
164
+ "type": "text",
165
+ "primaryKey": false,
166
+ "notNull": true,
167
+ "autoincrement": false
168
+ },
169
+ "counter": {
170
+ "name": "counter",
171
+ "type": "integer",
172
+ "primaryKey": false,
173
+ "notNull": true,
174
+ "autoincrement": false
175
+ },
176
+ "device_type": {
177
+ "name": "device_type",
178
+ "type": "text",
179
+ "primaryKey": false,
180
+ "notNull": true,
181
+ "autoincrement": false
182
+ },
183
+ "backed_up": {
184
+ "name": "backed_up",
185
+ "type": "integer",
186
+ "primaryKey": false,
187
+ "notNull": true,
188
+ "autoincrement": false
189
+ },
190
+ "transports": {
191
+ "name": "transports",
192
+ "type": "text",
193
+ "primaryKey": false,
194
+ "notNull": false,
195
+ "autoincrement": false
196
+ },
197
+ "created_at": {
198
+ "name": "created_at",
199
+ "type": "integer",
200
+ "primaryKey": false,
201
+ "notNull": false,
202
+ "autoincrement": false
203
+ },
204
+ "aaguid": {
205
+ "name": "aaguid",
206
+ "type": "text",
207
+ "primaryKey": false,
208
+ "notNull": false,
209
+ "autoincrement": false
210
+ }
211
+ },
212
+ "indexes": {
213
+ "passkey_userId_idx": {
214
+ "name": "passkey_userId_idx",
215
+ "columns": [
216
+ "user_id"
217
+ ],
218
+ "isUnique": false
219
+ },
220
+ "passkey_credentialID_idx": {
221
+ "name": "passkey_credentialID_idx",
222
+ "columns": [
223
+ "credential_id"
224
+ ],
225
+ "isUnique": false
226
+ }
227
+ },
228
+ "foreignKeys": {
229
+ "passkey_user_id_user_id_fk": {
230
+ "name": "passkey_user_id_user_id_fk",
231
+ "tableFrom": "passkey",
232
+ "tableTo": "user",
233
+ "columnsFrom": [
234
+ "user_id"
235
+ ],
236
+ "columnsTo": [
237
+ "id"
238
+ ],
239
+ "onDelete": "cascade",
240
+ "onUpdate": "no action"
241
+ }
242
+ },
243
+ "compositePrimaryKeys": {},
244
+ "uniqueConstraints": {},
245
+ "checkConstraints": {}
246
+ },
131
247
  "session": {
132
248
  "name": "session",
133
249
  "columns": {
@@ -231,6 +347,96 @@
231
347
  "uniqueConstraints": {},
232
348
  "checkConstraints": {}
233
349
  },
350
+ "two_factor": {
351
+ "name": "two_factor",
352
+ "columns": {
353
+ "id": {
354
+ "name": "id",
355
+ "type": "text",
356
+ "primaryKey": true,
357
+ "notNull": true,
358
+ "autoincrement": false
359
+ },
360
+ "secret": {
361
+ "name": "secret",
362
+ "type": "text",
363
+ "primaryKey": false,
364
+ "notNull": true,
365
+ "autoincrement": false
366
+ },
367
+ "backup_codes": {
368
+ "name": "backup_codes",
369
+ "type": "text",
370
+ "primaryKey": false,
371
+ "notNull": true,
372
+ "autoincrement": false
373
+ },
374
+ "user_id": {
375
+ "name": "user_id",
376
+ "type": "text",
377
+ "primaryKey": false,
378
+ "notNull": true,
379
+ "autoincrement": false
380
+ },
381
+ "verified": {
382
+ "name": "verified",
383
+ "type": "integer",
384
+ "primaryKey": false,
385
+ "notNull": false,
386
+ "autoincrement": false,
387
+ "default": true
388
+ },
389
+ "failed_verification_count": {
390
+ "name": "failed_verification_count",
391
+ "type": "integer",
392
+ "primaryKey": false,
393
+ "notNull": false,
394
+ "autoincrement": false,
395
+ "default": 0
396
+ },
397
+ "locked_until": {
398
+ "name": "locked_until",
399
+ "type": "integer",
400
+ "primaryKey": false,
401
+ "notNull": false,
402
+ "autoincrement": false
403
+ }
404
+ },
405
+ "indexes": {
406
+ "twoFactor_secret_idx": {
407
+ "name": "twoFactor_secret_idx",
408
+ "columns": [
409
+ "secret"
410
+ ],
411
+ "isUnique": false
412
+ },
413
+ "twoFactor_userId_idx": {
414
+ "name": "twoFactor_userId_idx",
415
+ "columns": [
416
+ "user_id"
417
+ ],
418
+ "isUnique": false
419
+ }
420
+ },
421
+ "foreignKeys": {
422
+ "two_factor_user_id_user_id_fk": {
423
+ "name": "two_factor_user_id_user_id_fk",
424
+ "tableFrom": "two_factor",
425
+ "tableTo": "user",
426
+ "columnsFrom": [
427
+ "user_id"
428
+ ],
429
+ "columnsTo": [
430
+ "id"
431
+ ],
432
+ "onDelete": "cascade",
433
+ "onUpdate": "no action"
434
+ }
435
+ },
436
+ "compositePrimaryKeys": {},
437
+ "uniqueConstraints": {},
438
+ "checkConstraints": {}
439
+ },
234
440
  "user": {
235
441
  "name": "user",
236
442
  "columns": {
@@ -314,6 +520,14 @@
314
520
  "primaryKey": false,
315
521
  "notNull": false,
316
522
  "autoincrement": false
523
+ },
524
+ "two_factor_enabled": {
525
+ "name": "two_factor_enabled",
526
+ "type": "integer",
527
+ "primaryKey": false,
528
+ "notNull": false,
529
+ "autoincrement": false,
530
+ "default": false
317
531
  }
318
532
  },
319
533
  "indexes": {
@@ -5,7 +5,7 @@
5
5
  {
6
6
  "idx": 0,
7
7
  "version": "6",
8
- "when": 1789674564602,
8
+ "when": 1790089765410,
9
9
  "tag": "0000_init",
10
10
  "breakpoints": true
11
11
  }