@plutocms/supabase 0.9.1 → 0.11.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 (41) hide show
  1. package/.prettierignore +2 -0
  2. package/CHANGELOG.md +19 -0
  3. package/FEATURES.md +9 -0
  4. package/README.md +10 -1
  5. package/app/components/EnvPersistWarning.vue +30 -13
  6. package/app/components/SupabaseMessageParts.vue +29 -0
  7. package/app/components/auth/AuthSignIn.vue +56 -0
  8. package/app/components/migrations/MigrationsBanner.vue +28 -15
  9. package/app/components/settings/SettingsGeneralPanel.vue +21 -6
  10. package/app/composables/auth.ts +38 -101
  11. package/app/pages/admin/(auth)/confirm.vue +20 -23
  12. package/app/pages/admin/(auth)/forgot-password.vue +60 -63
  13. package/app/pages/admin/(auth)/signup.vue +115 -110
  14. package/app/pages/admin/(auth)/update-password.vue +78 -82
  15. package/app/pages/admin/migrations.vue +87 -51
  16. package/app/pages/admin/setup.vue +88 -75
  17. package/app/plugins/pluto-extension.ts +143 -10
  18. package/app/utils/auth-driver.ts +110 -0
  19. package/app/utils/auth-error.ts +53 -0
  20. package/db/migrations/005_profile_locale.sql +44 -0
  21. package/i18n/locales/en.json +193 -0
  22. package/i18n/locales/pt-BR.json +193 -0
  23. package/package.json +3 -3
  24. package/server/api/profile/locale.get.ts +39 -0
  25. package/server/api/profile/locale.post.ts +71 -0
  26. package/server/api/setup/create.post.ts +6 -4
  27. package/server/api/users/[id].get.ts +4 -2
  28. package/server/api/users/index.get.ts +4 -2
  29. package/server/utils/admin-guard.ts +10 -2
  30. package/server/utils/capability-guard.ts +18 -4
  31. package/server/utils/content-adapter.ts +6 -4
  32. package/server/utils/pending-migrations.ts +0 -0
  33. package/server/utils/validate.ts +6 -1
  34. package/shared/types/supabase.ts +7 -0
  35. package/app/components/navbar/NavbarAdmin.vue +0 -213
  36. package/app/components/navbar/NavbarAdminProvider.vue +0 -18
  37. package/app/middleware/auth.ts +0 -34
  38. package/app/middleware/setup-check.ts +0 -44
  39. package/app/pages/admin/(auth)/login.vue +0 -93
  40. package/app/pages/admin.vue +0 -56
  41. package/app/plugins/pluto-permissions-sync.ts +0 -38
@@ -1,15 +1,135 @@
1
- import NavbarAdminProvider from '../components/navbar/NavbarAdminProvider.vue'
1
+ import AuthSignIn from '../components/auth/AuthSignIn.vue'
2
+ import MigrationsBanner from '../components/migrations/MigrationsBanner.vue'
2
3
  import SettingsGeneralPanel from '../components/settings/SettingsGeneralPanel.vue'
3
4
 
4
5
  export default defineNuxtPlugin(() => {
6
+ // Read at setup, while the Nuxt context exists. The driver below calls it
7
+ // later, after an await, when the context is gone.
8
+ const errorText = usePlutoErrorText()
9
+ const supabase = useSupabaseClient()
10
+ const session = useSupabaseSession()
11
+ const claims = useSupabaseUser()
12
+
13
+ // The site settings that the admin bar shows: the site name and the site
14
+ // URL. They load in the browser after a sign-in, because the settings read
15
+ // needs the session cookie. The state starts empty on the server.
16
+ const siteSettings = useState<Record<string, string>>(
17
+ 'supabase-site-settings',
18
+ () => ({})
19
+ )
20
+ const hasSettingsModified = useState<number>('has_settings_modified')
21
+
22
+ async function loadSiteSettings() {
23
+ try {
24
+ const response = await $fetch<{ settings: Record<string, string> }>(
25
+ '/api/settings'
26
+ )
27
+ siteSettings.value = response.settings
28
+ } catch {
29
+ // The bar then shows the fallback name and the "/" URL.
30
+ }
31
+ }
32
+
33
+ // The setup guard. A done setup is cached for the session, so the guard
34
+ // does not fetch on every navigation. A state that is not done is read
35
+ // again each time.
36
+ const setupDone = useState<boolean>('supabase-setup-done', () => false)
37
+
38
+ async function readSetupState(): Promise<SetupState> {
39
+ if (setupDone.value) {
40
+ return 'done'
41
+ }
42
+
43
+ let state: SetupState = 'unknown'
44
+ try {
45
+ state = setupStateFromResponse(
46
+ await $fetch('/api/settings/first_setup')
47
+ )
48
+ } catch (error) {
49
+ if (import.meta.dev) {
50
+ console.error('Error checking first setup status:', error)
51
+ }
52
+ }
53
+
54
+ setupDone.value = state === 'done'
55
+
56
+ return state
57
+ }
58
+
59
+ if (import.meta.client) {
60
+ watch(
61
+ [() => Boolean(session.value), hasSettingsModified],
62
+ ([signedIn]) => {
63
+ if (signedIn) {
64
+ void loadSiteSettings()
65
+ } else {
66
+ siteSettings.value = {}
67
+ }
68
+ },
69
+ { immediate: true }
70
+ )
71
+ }
72
+
5
73
  definePlutoExtension({
6
74
  id: 'supabase',
7
- navbar: { id: 'admin-navbar', component: NavbarAdminProvider },
75
+ // Core renders the navbar, the login page and the route guard from this
76
+ // driver. See the admin-shell skill in @plutocms/pluto.
77
+ authDriver: {
78
+ id: 'supabase-auth',
79
+ // The JWT claims carry the id (`sub`) and the user metadata.
80
+ user: () => mapSupabaseUser(claims.value),
81
+ // The session cookie gives the same answer on the server and in the
82
+ // first client render, so the bar does not cause a hydration mismatch.
83
+ loggedIn: () => Boolean(session.value),
84
+ // The module plugin reads the session before this plugin runs.
85
+ ready: () => Promise.resolve(),
86
+ refresh: async () => {
87
+ try {
88
+ const { data } = await supabase.auth.getSession()
89
+ session.value = data.session
90
+ if (data.session) {
91
+ const { data: claimsData } = await supabase.auth.getClaims()
92
+ claims.value = claimsData?.claims ?? null
93
+ } else {
94
+ claims.value = null
95
+ }
96
+ } catch {
97
+ // Keep the state as it is when the read fails.
98
+ }
99
+ },
100
+ signOut: async () => {
101
+ await supabase.auth.signOut()
102
+ session.value = null
103
+ claims.value = null
104
+ },
105
+ signIn: AuthSignIn,
106
+ signInTitle: 'supabase.auth.login.title',
107
+ // The old login page always showed the sign-up link.
108
+ links: {
109
+ signUp: '/admin/signup',
110
+ forgotPassword: '/admin/forgot-password',
111
+ },
112
+ publicPaths: SUPABASE_PUBLIC_PATHS,
113
+ // Core runs this on `/admin/**`, after ready() and before the sign-in
114
+ // rule, so the first-setup redirect wins over the sign-in redirect.
115
+ // It never rejects.
116
+ guard: async (to) => setupRedirect(to.path, await readSetupState()),
117
+ },
118
+ // The website title and URL settings. An empty value makes core use its
119
+ // own fallback (the runtime config name, and "/" for the URL).
120
+ site: {
121
+ id: 'supabase-site',
122
+ name: () => siteSettings.value.website_title,
123
+ // In development, "View site" opens this dev server, like the old navbar.
124
+ url: () => (import.meta.dev ? null : siteSettings.value.website_url),
125
+ },
126
+ // The pending-migrations notice, shown at the top of the admin pages.
127
+ adminBanners: [{ id: 'migrations', component: MigrationsBanner }],
8
128
  settingsPanels: [
9
129
  {
10
130
  id: 'general',
11
131
  order: 0,
12
- title: 'General',
132
+ title: 'supabase.settings.general.title',
13
133
  icon: 'lucide:globe',
14
134
  component: SettingsGeneralPanel,
15
135
  },
@@ -28,11 +148,7 @@ export default defineNuxtPlugin(() => {
28
148
  body: patch,
29
149
  })
30
150
 
31
- // NavbarAdmin.vue watches this to refresh its own /api/settings
32
- // read (it shows the site's own URL) — the settings page saving
33
- // through the new registry must still trigger it, exactly like
34
- // the old page did.
35
- const hasSettingsModified = useState<number>('has_settings_modified')
151
+ // The site info above watches this to read the settings again.
36
152
  hasSettingsModified.value = Date.now()
37
153
  },
38
154
  },
@@ -40,9 +156,26 @@ export default defineNuxtPlugin(() => {
40
156
  // gated by requireAdmin (public.is_admin() directly), never a named
41
157
  // capability — see server/utils/admin-guard.ts for why.
42
158
  capabilities: [
43
- { id: 'settings-manage', key: 'settings:manage', label: 'Manage site settings' },
44
- { id: 'users-read', key: 'users:read', label: 'View all user accounts' },
159
+ { id: 'settings-manage', key: 'settings:manage', label: 'supabase.capabilities.settingsManage' },
160
+ { id: 'users-read', key: 'users:read', label: 'supabase.capabilities.usersRead' },
45
161
  ],
162
+ // The admin language of the signed-in user, stored in profiles.locale
163
+ // (005_profile_locale.sql). A save error carries the translated server
164
+ // text, so the Settings language card can show it in a toast.
165
+ localeDriver: {
166
+ id: 'supabase',
167
+ load: async () => {
168
+ const response = await $fetch<{ locale: string | null }>('/api/profile/locale')
169
+ return response.locale
170
+ },
171
+ save: async (locale) => {
172
+ try {
173
+ await $fetch('/api/profile/locale', { method: 'POST', body: { locale } })
174
+ } catch (error) {
175
+ throw new Error(errorText(error))
176
+ }
177
+ },
178
+ },
46
179
  permissionsDriver: {
47
180
  id: 'supabase',
48
181
  load: async () => {
@@ -0,0 +1,110 @@
1
+ // Pure helpers of the supabase auth driver. This file does not use Nuxt
2
+ // auto-imports, so plain Vitest can import it. The `PlutoAuth*` types are
3
+ // global types of the pluto layer.
4
+ /**
5
+ * The fields of a Supabase user that the driver reads. It fits the JWT claims
6
+ * (`sub`, `email`, `user_metadata`) and a `User` object (`id`).
7
+ */
8
+ export interface SupabaseIdentity {
9
+ sub?: string
10
+ id?: string
11
+ email?: string | null
12
+ user_metadata?: Record<string, unknown> | null
13
+ }
14
+
15
+ function text(value: unknown): string | undefined {
16
+ return typeof value === 'string' && value.trim() !== ''
17
+ ? value.trim()
18
+ : undefined
19
+ }
20
+
21
+ /**
22
+ * Maps a Supabase user to the user of the admin shell. The name follows the
23
+ * order of the old navbar: display_name, full_name, name. In the JWT claims,
24
+ * the user id is `sub`. Returns null when there is no id.
25
+ */
26
+ export function mapSupabaseUser(
27
+ source: SupabaseIdentity | null | undefined
28
+ ): PlutoAuthUser | null {
29
+ const id = text(source?.sub) ?? text(source?.id)
30
+
31
+ if (!source || !id) {
32
+ return null
33
+ }
34
+
35
+ const meta = source.user_metadata ?? {}
36
+
37
+ return {
38
+ id,
39
+ name: text(meta.display_name) ?? text(meta.full_name) ?? text(meta.name),
40
+ username: text(meta.username),
41
+ email: text(source.email),
42
+ avatarUrl: text(meta.avatar_url) ?? text(meta.picture),
43
+ }
44
+ }
45
+
46
+ /**
47
+ * The admin paths that a signed-out visitor may open, with what happens to a
48
+ * signed-in user. `/admin/login` is always public in core.
49
+ *
50
+ * - `signup` and `forgot-password` send a signed-in user to the admin home.
51
+ * - `confirm` and `update-password` stay open: a recovery or confirm link
52
+ * signs the user in, and the page must still show.
53
+ * - `setup` stays open for the auth guard. The setup guard
54
+ * (`guard` in `pluto-extension.ts`) blocks it when the setup is done.
55
+ */
56
+ export const SUPABASE_PUBLIC_PATHS: PlutoAuthPublicPath[] = [
57
+ { path: '/admin/signup', signedIn: 'redirect' },
58
+ { path: '/admin/forgot-password', signedIn: 'redirect' },
59
+ { path: '/admin/confirm', signedIn: 'allow' },
60
+ { path: '/admin/update-password', signedIn: 'allow' },
61
+ { path: '/admin/setup', signedIn: 'allow' },
62
+ ]
63
+
64
+ /** The page that opens after a sign-in with no (safe) redirect. */
65
+ export const SUPABASE_SIGN_IN_FALLBACK = '/admin/home'
66
+
67
+ /** The setup state that the setup guard reads from `/api/settings/first_setup`. */
68
+ export type SetupState = 'pending' | 'done' | 'unknown'
69
+
70
+ /**
71
+ * Reads the answer of `/api/settings/first_setup`. A failed call, or a
72
+ * response with no `is_first_setup` flag, is `unknown`.
73
+ */
74
+ export function setupStateFromResponse(data: unknown): SetupState {
75
+ const response = data as
76
+ | { success?: boolean, is_first_setup?: boolean }
77
+ | null
78
+ | undefined
79
+
80
+ if (!response?.success) {
81
+ return 'unknown'
82
+ }
83
+
84
+ return response.is_first_setup ? 'pending' : 'done'
85
+ }
86
+
87
+ /**
88
+ * Decides the setup redirect for an admin path. It gives a path to open, or
89
+ * undefined to go on.
90
+ *
91
+ * - `pending` and `unknown` (the state cannot be read): only the setup page
92
+ * is open. Every other admin path goes to `/admin/setup`.
93
+ * - `done`: the setup page is closed, and goes to the sign-in page.
94
+ *
95
+ * Keep the `done` redirect, even after layer migrations move to
96
+ * /admin/migrations. The setup page prompts an unauthenticated visitor for a
97
+ * raw database connection string, and it is safe only because it is
98
+ * unreachable once setup is done. Reopening it for a routine layer addition
99
+ * would let an anonymous visitor submit a connection string to the server.
100
+ */
101
+ export function setupRedirect(
102
+ path: string,
103
+ state: SetupState
104
+ ): string | undefined {
105
+ if (state === 'done') {
106
+ return path === '/admin/setup' ? '/admin/login' : undefined
107
+ }
108
+
109
+ return path === '/admin/setup' ? undefined : '/admin/setup'
110
+ }
@@ -0,0 +1,53 @@
1
+ // Turns a Supabase auth error into text for the admin. This file does not use
2
+ // Nuxt auto-imports, so plain Vitest can import it.
3
+
4
+ /** The translation key of each Supabase auth error code that the admin knows. */
5
+ export const AUTH_ERROR_KEYS: Record<string, string> = {
6
+ invalid_credentials: 'supabase.auth.errors.invalid_credentials',
7
+ email_not_confirmed: 'supabase.auth.errors.email_not_confirmed',
8
+ over_request_rate_limit: 'supabase.auth.errors.over_request_rate_limit',
9
+ over_email_send_rate_limit: 'supabase.auth.errors.over_email_send_rate_limit',
10
+ weak_password: 'supabase.auth.errors.weak_password',
11
+ same_password: 'supabase.auth.errors.same_password',
12
+ user_not_found: 'supabase.auth.errors.user_not_found',
13
+ email_exists: 'supabase.auth.errors.email_exists',
14
+ user_already_exists: 'supabase.auth.errors.user_already_exists',
15
+ signup_disabled: 'supabase.auth.errors.signup_disabled',
16
+ email_address_invalid: 'supabase.auth.errors.email_address_invalid',
17
+ }
18
+
19
+ /** The key for errors with no code, no message, or an unknown shape. */
20
+ export const AUTH_UNKNOWN_ERROR_KEY = 'supabase.auth.errors.unknown'
21
+
22
+ /** Returns the translation key of an auth error code, or `undefined`. */
23
+ export function authErrorKey(error: unknown): string | undefined {
24
+ const code = (error as { code?: unknown } | null | undefined)?.code
25
+
26
+ return typeof code === 'string' && Object.hasOwn(AUTH_ERROR_KEYS, code)
27
+ ? AUTH_ERROR_KEYS[code]
28
+ : undefined
29
+ }
30
+
31
+ /**
32
+ * Returns the text of an auth error. The order is:
33
+ *
34
+ * 1. The translated text of a known `error.code`.
35
+ * 2. The raw `error.message`.
36
+ * 3. The translated unknown-error text.
37
+ */
38
+ export function resolveAuthErrorText(
39
+ error: unknown,
40
+ i18n: { t: (key: string) => string, te: (key: string) => boolean }
41
+ ): string {
42
+ const key = authErrorKey(error)
43
+
44
+ if (key && i18n.te(key)) {
45
+ return i18n.t(key)
46
+ }
47
+
48
+ const message = (error as { message?: unknown } | null | undefined)?.message
49
+
50
+ return typeof message === 'string' && message.trim() !== ''
51
+ ? message
52
+ : i18n.t(AUTH_UNKNOWN_ERROR_KEY)
53
+ }
@@ -0,0 +1,44 @@
1
+ -- Profile language
2
+ --
3
+ -- Stores each user's admin language on their own profile row. NULL means
4
+ -- "Automatic": the user has not chosen one, so the admin uses the browser
5
+ -- language. The format check accepts any BCP 47 style code ('en', 'pt-BR'),
6
+ -- never a fixed list, so a new language needs no migration. The app decides
7
+ -- which codes it offers.
8
+ --
9
+ -- There is no UPDATE policy on public.profiles, on purpose: a user must never
10
+ -- write their own is_admin column. A SECURITY DEFINER function is the only
11
+ -- writer of this column. It touches the row of auth.uid() and no other row,
12
+ -- and it touches only the locale. This migration adds no RLS policy. The
13
+ -- existing select policy already lets a user read their own row.
14
+
15
+ alter table public.profiles add column if not exists locale text;
16
+
17
+ do $$
18
+ begin
19
+ if not exists (
20
+ select 1 from pg_constraint
21
+ where conname = 'profiles_locale_format'
22
+ and conrelid = 'public.profiles'::regclass
23
+ ) then
24
+ alter table public.profiles
25
+ add constraint profiles_locale_format
26
+ check (locale is null or locale ~ '^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$') not valid;
27
+ end if;
28
+ end
29
+ $$;
30
+
31
+ alter table public.profiles validate constraint profiles_locale_format;
32
+
33
+ -- Sets the language of the calling user. Pass NULL to choose Automatic.
34
+ create or replace function public.set_my_locale(new_locale text)
35
+ returns void
36
+ language sql security definer set search_path = ''
37
+ as $$
38
+ update public.profiles
39
+ set locale = new_locale, updated_at = now()
40
+ where id = auth.uid();
41
+ $$;
42
+
43
+ revoke execute on function public.set_my_locale(text) from public, anon;
44
+ grant execute on function public.set_my_locale(text) to authenticated;
@@ -0,0 +1,193 @@
1
+ {
2
+ "supabase": {
3
+ "profile": {
4
+ "migrationPending": "Apply the pending database migrations to save your language."
5
+ },
6
+ "errors": {
7
+ "notLoggedIn": "You must be logged in.",
8
+ "notAdmin": "Your account is not an admin.",
9
+ "missingPermission": "Your account is missing the \"{capability}\" permission.",
10
+ "setupCompleted": "Setup has already been completed.",
11
+ "coreSchemaMissing": "Core schema not found. Reinstall @plutocms/supabase and restart the dev server.",
12
+ "fetchUsers": "Error fetching users: {message}",
13
+ "fetchUser": "Error fetching user: {message}",
14
+ "localeUnsupported": "This language is not supported.",
15
+ "duplicateRecord": "A record with this value already exists.",
16
+ "missingReference": "This references a record that does not exist.",
17
+ "migrationStatus": "Error reading migration status: {message}"
18
+ },
19
+ "auth": {
20
+ "backToLogin": "Back to Login",
21
+ "passwordMismatch": "Passwords do not match.",
22
+ "fields": {
23
+ "email": "Email",
24
+ "password": "Password",
25
+ "username": "Username",
26
+ "displayName": "Display Name",
27
+ "confirmPassword": "Confirm Password"
28
+ },
29
+ "toasts": {
30
+ "loginFailed": "Could not log in",
31
+ "resetFailed": "Could not send reset email",
32
+ "updateFailed": "Could not update password",
33
+ "passwordUpdated": "Password updated successfully"
34
+ },
35
+ "errors": {
36
+ "unknown": "An unknown error occurred.",
37
+ "invalid_credentials": "Invalid email or password.",
38
+ "email_not_confirmed": "Your email is not confirmed yet. Check your inbox for the verification link.",
39
+ "over_request_rate_limit": "Too many attempts. Wait a moment and try again.",
40
+ "over_email_send_rate_limit": "Too many emails sent. Wait a moment and try again.",
41
+ "weak_password": "This password is too weak. Choose a stronger one.",
42
+ "same_password": "The new password must be different from the current one.",
43
+ "user_not_found": "No account was found for this email.",
44
+ "email_exists": "An account with this email already exists.",
45
+ "user_already_exists": "An account with this email already exists.",
46
+ "signup_disabled": "Sign up is turned off for this site.",
47
+ "email_address_invalid": "This email address is not valid."
48
+ },
49
+ "confirm": {
50
+ "pageTitle": "Account Verification",
51
+ "title": "Account verified!",
52
+ "description": "Your account has been successfully verified. You can now start using the Dashboard.",
53
+ "goToDashboard": "Go to Dashboard"
54
+ },
55
+ "forgot": {
56
+ "pageTitle": "Forgot Password",
57
+ "title": "Forgot Password",
58
+ "description": "Enter your email and we'll send you a reset link.",
59
+ "submit": "Send Reset Link",
60
+ "sentTitle": "Check your inbox",
61
+ "sentDescription": "If an account exists for {email}, a password reset link has been sent. Check your email and follow the link."
62
+ },
63
+ "login": {
64
+ "title": "Login",
65
+ "submit": "Login"
66
+ },
67
+ "update": {
68
+ "title": "Update Password",
69
+ "description": "Enter your new password below.",
70
+ "newPassword": "New Password",
71
+ "confirmNewPassword": "Confirm New Password",
72
+ "submit": "Update Password",
73
+ "doneTitle": "Password Updated!",
74
+ "doneDescription": "Your password has been changed. You can now log in with your new password.",
75
+ "goToLogin": "Go to Login"
76
+ },
77
+ "signup": {
78
+ "pageTitle": "Sign Up",
79
+ "title": "Create Account",
80
+ "submit": "Create account",
81
+ "usernamePlaceholder": "e.g: johndoe",
82
+ "displayNamePlaceholder": "e.g: John Doe",
83
+ "created": "Account created successfully",
84
+ "createdDescription": "You still need to verify your email before you can log in.",
85
+ "failed": "Could not create account",
86
+ "almostThere": "Almost there!",
87
+ "verificationSent": "A verification link has been sent to your email. Please check your inbox and follow the instructions to verify your account.",
88
+ "closePage": "You can close this page. You will be logged in automatically once you verify your email.",
89
+ "checkSpam": "If you don't see the email, please check your spam folder."
90
+ }
91
+ },
92
+ "settings": {
93
+ "general": {
94
+ "title": "General",
95
+ "websiteTitle": "Website Title",
96
+ "websiteTitlePlaceholder": "e.g. My Website",
97
+ "websiteDescription": "Website Description",
98
+ "websiteDescriptionPlaceholder": "e.g. My Website Description",
99
+ "websiteUrl": "Website URL",
100
+ "websiteUrlPlaceholder": "e.g. https://example.com"
101
+ }
102
+ },
103
+ "capabilities": {
104
+ "settingsManage": "Manage site settings",
105
+ "usersRead": "View all user accounts"
106
+ },
107
+ "envWarning": {
108
+ "title": "Connection string not saved automatically",
109
+ "intro": "The database change already succeeded. Pluto could not write {variable} to your project's {file} file, so it will ask again next time unless you add it by hand:",
110
+ "step1": "Open (or create) {file} in your project's root folder.",
111
+ "step2": "Add this line. Replace any existing {variable} line with it.",
112
+ "step3": "Restart the server.",
113
+ "copyLine": "Copy the DATABASE_URL line",
114
+ "copied": "Copied",
115
+ "copiedDescription": "The DATABASE_URL line is on your clipboard.",
116
+ "copyFailed": "Could not copy",
117
+ "copyFailedDescription": "Select and copy the line by hand instead."
118
+ },
119
+ "connection": {
120
+ "connectionString": "Supabase connection string",
121
+ "connectionStringHelp": "You can find your connection string {link}.",
122
+ "connectionStringHelpLink": "clicking here",
123
+ "password": "Database password",
124
+ "passwordInfo": "Your password is created when you create your Supabase project. You can change it in the {link}.",
125
+ "passwordInfoLink": "Supabase Database Settings",
126
+ "whyTitle": "Why do I need to provide a database password?",
127
+ "restartTitle": "This restarts the dev server",
128
+ "restarting": "Dev server restarting",
129
+ "reconnected": "Reconnected",
130
+ "stillReconnecting": "Still reconnecting",
131
+ "stillReconnectingDescription": "The dev server is taking longer than expected. Reload this page in a moment.",
132
+ "checkConsole": "Please check the console for more details."
133
+ },
134
+ "setup": {
135
+ "title": "Setup Wizard",
136
+ "steps": {
137
+ "database": "Database Setup",
138
+ "complete": "Complete Setup"
139
+ },
140
+ "submit": "Set up database",
141
+ "next": "Submit",
142
+ "why": "It will be used to create your database structure. It won't be stored anywhere. That's a {promise}! 😉",
143
+ "promise": "promise",
144
+ "promiseTooltip": "Click on the 'Technical details' button for more info",
145
+ "technicalDetails": "Technical details",
146
+ "technicalDetailsRequest": "When you click \"Next\", a request to {route} will be made with the database password you provided. This API will then use this password to make a direct PostgreSQL connection to your Supabase database and run the SQL queries from this {files}.",
147
+ "technicalDetailsRoute": "a server route",
148
+ "technicalDetailsFiles": "SQL files",
149
+ "technicalDetailsStored": "After the setup is complete, the password will NOT be stored or used for any other purpose.",
150
+ "restartDescription": "Saving your connection string writes it to .env. In development, that restarts the server automatically. The page will briefly show a reconnecting message — this is expected, not an error.",
151
+ "restartingDescription": "Setup is finishing. Reconnecting…",
152
+ "reconnectedDescription": "The server is back. Your database has been set up.",
153
+ "failed": "Database setup failed",
154
+ "failedDescription": "Please check your password and try again.",
155
+ "complete": "Database setup complete",
156
+ "completeDescription": "Your database has been set up successfully.",
157
+ "error": "Error setting up the database",
158
+ "doneTitle": "Setup Complete",
159
+ "doneDescription": "Your database is ready. Create your admin account by signing up.",
160
+ "goToSignup": "Go to sign up"
161
+ },
162
+ "migrations": {
163
+ "title": "Migrations",
164
+ "banner": "{count} layer needs a database migration | {count} layers need a database migration",
165
+ "goTo": "Go to Migrations",
166
+ "loading": "Loading migration status…",
167
+ "upToDate": "Everything is up to date",
168
+ "upToDateDescription": "No layer needs a database migration.",
169
+ "pendingLayers": "Pending layers ({count})",
170
+ "appliedLayers": "Applied layers ({count})",
171
+ "why": "It is used once to open a direct PostgreSQL connection and run the pending layer schemas. It is then written to .env on the server, so future migrations need no prompt. It is never stored anywhere else, and never sent back to your browser.",
172
+ "restartDescription": "Saving a new connection string writes it to .env. In development, that restarts the server automatically. The page will briefly show a reconnecting message — this is expected, not an error.",
173
+ "restartingDescription": "Saving a new connection string restarts the dev server. Reconnecting…",
174
+ "reconnectingBanner": "Saving a new connection string restarts the dev server. Reconnecting — this page will update on its own once it's back.",
175
+ "reconnectedAllApplied": "The server is back. All migrations were applied.",
176
+ "reconnectedPending": "The server is back. {count} layer(s) still pending — check above.",
177
+ "apply": "Apply pending migrations",
178
+ "applied": "Migrations applied",
179
+ "applyFailed": "Could not apply migrations",
180
+ "connectionRequired": "A database connection string is required.",
181
+ "error": "Error applying migrations",
182
+ "lastRun": "Last run",
183
+ "status": {
184
+ "applied": "applied",
185
+ "skipped": "skipped",
186
+ "failed": "failed"
187
+ },
188
+ "loadFailed": "Could not load migration status",
189
+ "loadFailedDescription": "This isn't the dev-server-restart case above — something else went wrong reading migration status. Check the server console, then try again.",
190
+ "retry": "Retry"
191
+ }
192
+ }
193
+ }