@plutocms/supabase 0.10.0 → 0.11.1
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.
- package/CHANGELOG.md +24 -0
- package/FEATURES.md +3 -0
- package/README.md +10 -1
- package/app/components/auth/AuthSignIn.vue +56 -0
- package/app/components/migrations/MigrationsBanner.vue +26 -12
- package/app/composables/auth.ts +22 -88
- package/app/pages/admin/(auth)/confirm.vue +17 -21
- package/app/pages/admin/(auth)/forgot-password.vue +57 -64
- package/app/pages/admin/(auth)/signup.vue +104 -107
- package/app/pages/admin/(auth)/update-password.vue +75 -82
- package/app/pages/admin/setup.vue +0 -1
- package/app/plugins/pluto-extension.ts +147 -8
- package/app/utils/auth-driver.ts +110 -0
- package/i18n/locales/en.json +2 -18
- package/i18n/locales/pt-BR.json +2 -18
- package/package.json +2 -2
- package/server/utils/typegen.ts +67 -2
- package/app/components/navbar/NavbarAdmin.vue +0 -219
- package/app/components/navbar/NavbarAdminProvider.vue +0 -18
- package/app/middleware/auth.ts +0 -34
- package/app/middleware/setup-check.ts +0 -44
- package/app/pages/admin/(auth)/login.vue +0 -95
- package/app/pages/admin.vue +0 -57
- package/app/plugins/pluto-locale-sync.client.ts +0 -30
- package/app/plugins/pluto-permissions-sync.ts +0 -38
|
@@ -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
|
+
}
|
package/i18n/locales/en.json
CHANGED
|
@@ -28,12 +28,9 @@
|
|
|
28
28
|
},
|
|
29
29
|
"toasts": {
|
|
30
30
|
"loginFailed": "Could not log in",
|
|
31
|
-
"loggedOut": "Logged out successfully",
|
|
32
31
|
"resetFailed": "Could not send reset email",
|
|
33
32
|
"updateFailed": "Could not update password",
|
|
34
|
-
"passwordUpdated": "Password updated successfully"
|
|
35
|
-
"notLoggedIn": "You are not logged in",
|
|
36
|
-
"notLoggedInDescription": "Please log in to continue."
|
|
33
|
+
"passwordUpdated": "Password updated successfully"
|
|
37
34
|
},
|
|
38
35
|
"errors": {
|
|
39
36
|
"unknown": "An unknown error occurred.",
|
|
@@ -65,10 +62,7 @@
|
|
|
65
62
|
},
|
|
66
63
|
"login": {
|
|
67
64
|
"title": "Login",
|
|
68
|
-
"submit": "Login"
|
|
69
|
-
"createAccount": "Create Account",
|
|
70
|
-
"backToHome": "Back to Home",
|
|
71
|
-
"forgotPassword": "Forgot password?"
|
|
65
|
+
"submit": "Login"
|
|
72
66
|
},
|
|
73
67
|
"update": {
|
|
74
68
|
"title": "Update Password",
|
|
@@ -95,16 +89,6 @@
|
|
|
95
89
|
"checkSpam": "If you don't see the email, please check your spam folder."
|
|
96
90
|
}
|
|
97
91
|
},
|
|
98
|
-
"navbar": {
|
|
99
|
-
"defaultUserName": "User",
|
|
100
|
-
"github": "Pluto on GitHub",
|
|
101
|
-
"logout": "Logout",
|
|
102
|
-
"openNavigation": "Open navigation",
|
|
103
|
-
"goHome": "Go to dashboard home",
|
|
104
|
-
"visitSite": "Visit {domain}",
|
|
105
|
-
"viewSite": "View site",
|
|
106
|
-
"openUserMenu": "Open user menu"
|
|
107
|
-
},
|
|
108
92
|
"settings": {
|
|
109
93
|
"general": {
|
|
110
94
|
"title": "General",
|
package/i18n/locales/pt-BR.json
CHANGED
|
@@ -28,12 +28,9 @@
|
|
|
28
28
|
},
|
|
29
29
|
"toasts": {
|
|
30
30
|
"loginFailed": "Não foi possível entrar",
|
|
31
|
-
"loggedOut": "Sessão encerrada com sucesso",
|
|
32
31
|
"resetFailed": "Não foi possível enviar o e-mail de redefinição",
|
|
33
32
|
"updateFailed": "Não foi possível atualizar a senha",
|
|
34
|
-
"passwordUpdated": "Senha atualizada com sucesso"
|
|
35
|
-
"notLoggedIn": "Você não está conectado",
|
|
36
|
-
"notLoggedInDescription": "Entre na sua conta para continuar."
|
|
33
|
+
"passwordUpdated": "Senha atualizada com sucesso"
|
|
37
34
|
},
|
|
38
35
|
"errors": {
|
|
39
36
|
"unknown": "Ocorreu um erro desconhecido.",
|
|
@@ -65,10 +62,7 @@
|
|
|
65
62
|
},
|
|
66
63
|
"login": {
|
|
67
64
|
"title": "Entrar",
|
|
68
|
-
"submit": "Entrar"
|
|
69
|
-
"createAccount": "Criar conta",
|
|
70
|
-
"backToHome": "Voltar para o início",
|
|
71
|
-
"forgotPassword": "Esqueceu a senha?"
|
|
65
|
+
"submit": "Entrar"
|
|
72
66
|
},
|
|
73
67
|
"update": {
|
|
74
68
|
"title": "Atualizar senha",
|
|
@@ -95,16 +89,6 @@
|
|
|
95
89
|
"checkSpam": "Se você não encontrar o e-mail, verifique sua pasta de spam."
|
|
96
90
|
}
|
|
97
91
|
},
|
|
98
|
-
"navbar": {
|
|
99
|
-
"defaultUserName": "Usuário",
|
|
100
|
-
"github": "Pluto no GitHub",
|
|
101
|
-
"logout": "Sair",
|
|
102
|
-
"openNavigation": "Abrir navegação",
|
|
103
|
-
"goHome": "Ir para o início do painel",
|
|
104
|
-
"visitSite": "Visitar {domain}",
|
|
105
|
-
"viewSite": "Ver site",
|
|
106
|
-
"openUserMenu": "Abrir menu do usuário"
|
|
107
|
-
},
|
|
108
92
|
"settings": {
|
|
109
93
|
"general": {
|
|
110
94
|
"title": "Geral",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plutocms/supabase",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.1",
|
|
5
5
|
"trustedDependencies": [
|
|
6
6
|
"@parcel/watcher",
|
|
7
7
|
"@plutocms/pluto",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@nuxt/eslint": "^1.17.0",
|
|
40
40
|
"@nuxt/ui": "^4.12.0",
|
|
41
41
|
"@nuxtjs/supabase": "^2.1.0",
|
|
42
|
-
"@plutocms/pluto": "^0.
|
|
42
|
+
"@plutocms/pluto": "^0.17.0",
|
|
43
43
|
"@plutocms/utils": "^0.2.1",
|
|
44
44
|
"postgres": "^3.4.9",
|
|
45
45
|
"supabase": "^2.121.0",
|
package/server/utils/typegen.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process'
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
3
4
|
import { join } from 'node:path'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
4
6
|
import { promisify } from 'node:util'
|
|
5
7
|
|
|
6
8
|
const execFileAsync = promisify(execFile)
|
|
@@ -17,6 +19,63 @@ function resolveSupabaseBin(rootDir: string): string {
|
|
|
17
19
|
return existsSync(localBin) ? localBin : 'supabase'
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
// The block, with its leading comment lines. The block has no nested braces.
|
|
23
|
+
const INTERNAL_BLOCK = /(?:^[ \t]*\/\/[^\n]*\n)*^[ \t]*__InternalSupabase:\s*\{[^{}]*\}[ \t]*\n/m
|
|
24
|
+
const DATABASE_OPEN = /^export type Database = \{[ \t]*\n/m
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Keeps the `__InternalSupabase` block of the existing types file.
|
|
28
|
+
*
|
|
29
|
+
* The `--db-url` mode of the Supabase CLI does not write this block. It
|
|
30
|
+
* holds the PostgREST version that supabase-js reads for its typings. When
|
|
31
|
+
* the new output has no block and the existing file has one, this function
|
|
32
|
+
* copies the existing block into the new output. In every other case it
|
|
33
|
+
* returns the new output as it is.
|
|
34
|
+
*/
|
|
35
|
+
export function keepInternalSupabase(
|
|
36
|
+
next: string,
|
|
37
|
+
existing: string | null
|
|
38
|
+
): string {
|
|
39
|
+
if (!existing || next.includes('__InternalSupabase')) {
|
|
40
|
+
return next
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const block = existing.match(INTERNAL_BLOCK)?.[0]
|
|
44
|
+
const open = next.match(DATABASE_OPEN)
|
|
45
|
+
|
|
46
|
+
if (!block || !open || open.index === undefined) {
|
|
47
|
+
return next
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const at = open.index + open[0].length
|
|
51
|
+
|
|
52
|
+
return next.slice(0, at) + block + next.slice(at)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Formats the text with the Prettier of the app. It resolves Prettier from
|
|
57
|
+
* `rootDir`. It returns the text unchanged when the app has no Prettier or
|
|
58
|
+
* when the format fails.
|
|
59
|
+
*/
|
|
60
|
+
async function formatWithAppPrettier(
|
|
61
|
+
rootDir: string,
|
|
62
|
+
filePath: string,
|
|
63
|
+
text: string
|
|
64
|
+
): Promise<string> {
|
|
65
|
+
try {
|
|
66
|
+
const entry = createRequire(join(rootDir, 'package.json')).resolve(
|
|
67
|
+
'prettier'
|
|
68
|
+
)
|
|
69
|
+
const prettier = await import(pathToFileURL(entry).href)
|
|
70
|
+
const api = prettier.default ?? prettier
|
|
71
|
+
const options = (await api.resolveConfig(filePath)) ?? {}
|
|
72
|
+
|
|
73
|
+
return await api.format(text, { ...options, filepath: filePath })
|
|
74
|
+
} catch {
|
|
75
|
+
return text
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
20
79
|
/**
|
|
21
80
|
* Regenerates the Supabase `Database` types from the live database schema
|
|
22
81
|
* and writes them to <rootDir>/shared/types/supabase.ts, only touching the
|
|
@@ -56,11 +115,17 @@ export async function regenerateSupabaseTypes(
|
|
|
56
115
|
? readFileSync(targetPath, 'utf-8')
|
|
57
116
|
: null
|
|
58
117
|
|
|
59
|
-
|
|
118
|
+
const processed = await formatWithAppPrettier(
|
|
119
|
+
rootDir,
|
|
120
|
+
targetPath,
|
|
121
|
+
keepInternalSupabase(stdout, existing)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if (existing === processed) {
|
|
60
125
|
return
|
|
61
126
|
}
|
|
62
127
|
|
|
63
|
-
writeFileSync(targetPath,
|
|
128
|
+
writeFileSync(targetPath, processed, 'utf-8')
|
|
64
129
|
console.warn(`[typegen] Regenerated Supabase types at ${targetPath}`)
|
|
65
130
|
} catch (error) {
|
|
66
131
|
console.warn('[typegen] Failed to regenerate Supabase types:', error)
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { UDropdownMenu } from '#components'
|
|
3
|
-
import { domainFromUrl } from '#imports'
|
|
4
|
-
|
|
5
|
-
const host = useRequestURL().host
|
|
6
|
-
|
|
7
|
-
const route = useRoute()
|
|
8
|
-
const isSidebarOpen = useState<boolean>('pluto-admin-sidebar-open', () => false)
|
|
9
|
-
|
|
10
|
-
function openSidebar() {
|
|
11
|
-
isSidebarOpen.value = true
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const { t } = usePlutoI18n()
|
|
15
|
-
|
|
16
|
-
const { isLoggedIn, user, logout } = await useAuth()
|
|
17
|
-
|
|
18
|
-
const displayName = computed(() => {
|
|
19
|
-
return (
|
|
20
|
-
user.value?.display_name ||
|
|
21
|
-
user.value?.full_name ||
|
|
22
|
-
user.value?.name ||
|
|
23
|
-
user.value?.username ||
|
|
24
|
-
t('supabase.navbar.defaultUserName')
|
|
25
|
-
)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
const shortDisplayName = computed(() => displayName.value.split(' ')[0])
|
|
29
|
-
|
|
30
|
-
const userMenuLabel = computed(() => {
|
|
31
|
-
return user.value?.username
|
|
32
|
-
? `${displayName.value} (${user.value.username})`
|
|
33
|
-
: displayName.value
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
const has_settings_modified = useState<boolean>('has_settings_modified')
|
|
37
|
-
|
|
38
|
-
const { data: settingsData, status } = await useFetch('/api/settings', {
|
|
39
|
-
watch: [has_settings_modified],
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
const maybeDevUrl = computed(() => {
|
|
43
|
-
if (import.meta.dev) {
|
|
44
|
-
return `http://${host}`
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return settingsData.value?.settings.website_url || ''
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
type DropdownItems = NonNullable<
|
|
51
|
-
Parameters<typeof UDropdownMenu>[0]['items']
|
|
52
|
-
>
|
|
53
|
-
|
|
54
|
-
const items = computed<DropdownItems>(() => [
|
|
55
|
-
[
|
|
56
|
-
{
|
|
57
|
-
label: userMenuLabel.value,
|
|
58
|
-
avatar: {
|
|
59
|
-
icon: 'lucide:user',
|
|
60
|
-
class: 'bg-primary',
|
|
61
|
-
ui: {
|
|
62
|
-
icon: 'text-inverted text-sm',
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
type: 'label',
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
|
|
69
|
-
[
|
|
70
|
-
{
|
|
71
|
-
label: t('pluto.common.settings'),
|
|
72
|
-
icon: 'i-lucide-cog',
|
|
73
|
-
type: 'link',
|
|
74
|
-
to: '/admin/settings',
|
|
75
|
-
kbds: ['meta', ','],
|
|
76
|
-
onSelect() {
|
|
77
|
-
if (isLoggedIn.value) {
|
|
78
|
-
navigateTo('/admin/settings')
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
],
|
|
83
|
-
|
|
84
|
-
[
|
|
85
|
-
{
|
|
86
|
-
label: t('supabase.navbar.github'),
|
|
87
|
-
icon: 'i-simple-icons-github',
|
|
88
|
-
type: 'link',
|
|
89
|
-
to: 'https://github.com/ojvribeiro/pluto',
|
|
90
|
-
target: '_blank',
|
|
91
|
-
external: true,
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
|
|
95
|
-
[
|
|
96
|
-
{
|
|
97
|
-
label: t('supabase.navbar.logout'),
|
|
98
|
-
icon: 'i-lucide-log-out',
|
|
99
|
-
kbds: ['meta', 'shift', 'q'],
|
|
100
|
-
class: 'cursor-pointer',
|
|
101
|
-
onSelect() {
|
|
102
|
-
logout({ showToast: true })
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
])
|
|
107
|
-
|
|
108
|
-
defineShortcuts(extractShortcuts(items.value))
|
|
109
|
-
</script>
|
|
110
|
-
|
|
111
|
-
<template>
|
|
112
|
-
<PlutoNavbarAdmin>
|
|
113
|
-
<div
|
|
114
|
-
class="flex h-full min-w-0 items-stretch justify-between gap-1 px-2 lg:px-4"
|
|
115
|
-
>
|
|
116
|
-
<div class="flex min-w-0 items-center gap-x-1 lg:gap-x-3">
|
|
117
|
-
<UButton
|
|
118
|
-
:aria-label="t('supabase.navbar.openNavigation')"
|
|
119
|
-
class="lg:hidden"
|
|
120
|
-
icon="lucide:menu"
|
|
121
|
-
color="neutral"
|
|
122
|
-
variant="ghost"
|
|
123
|
-
square
|
|
124
|
-
@click="openSidebar"
|
|
125
|
-
/>
|
|
126
|
-
|
|
127
|
-
<div class="h-full shrink-0 py-1">
|
|
128
|
-
<NuxtLink
|
|
129
|
-
:title="t('supabase.navbar.goHome')"
|
|
130
|
-
to="/admin/home"
|
|
131
|
-
class="group block h-full"
|
|
132
|
-
>
|
|
133
|
-
<img
|
|
134
|
-
src="/img/pluto.png"
|
|
135
|
-
class="block h-full grayscale-100 group-hover:grayscale-0"
|
|
136
|
-
/>
|
|
137
|
-
</NuxtLink>
|
|
138
|
-
</div>
|
|
139
|
-
|
|
140
|
-
<div class="min-w-0 h-full overflow-hidden">
|
|
141
|
-
<ul class="flex h-full min-w-0 items-center text-sm">
|
|
142
|
-
<NavbarAdminActionButton
|
|
143
|
-
:show="route.path.startsWith('/admin')"
|
|
144
|
-
:title="
|
|
145
|
-
t('supabase.navbar.visitSite', {
|
|
146
|
-
domain: domainFromUrl(maybeDevUrl),
|
|
147
|
-
})
|
|
148
|
-
"
|
|
149
|
-
:icon="
|
|
150
|
-
status === 'pending'
|
|
151
|
-
? 'lucide:loader-circle'
|
|
152
|
-
: 'lucide:external-link'
|
|
153
|
-
"
|
|
154
|
-
:disabled="status === 'pending'"
|
|
155
|
-
:to="maybeDevUrl"
|
|
156
|
-
:label="t('supabase.navbar.viewSite')"
|
|
157
|
-
target="_blank"
|
|
158
|
-
/>
|
|
159
|
-
|
|
160
|
-
<ClientOnly>
|
|
161
|
-
<template #fallback>
|
|
162
|
-
<li class="h-full">
|
|
163
|
-
<div
|
|
164
|
-
class="text-dimmed flex h-full items-center gap-x-2 rounded-sm px-2"
|
|
165
|
-
>
|
|
166
|
-
<Icon name="svg-spinners:ring-resize" />
|
|
167
|
-
</div>
|
|
168
|
-
</li>
|
|
169
|
-
</template>
|
|
170
|
-
|
|
171
|
-
<PlutoNavbarAdminActions />
|
|
172
|
-
</ClientOnly>
|
|
173
|
-
</ul>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
|
|
177
|
-
<div class="flex shrink-0 items-center gap-1 lg:gap-3">
|
|
178
|
-
<ColorModeButton />
|
|
179
|
-
|
|
180
|
-
<div class="h-full">
|
|
181
|
-
<UDropdownMenu
|
|
182
|
-
:items="items"
|
|
183
|
-
:ui="{
|
|
184
|
-
content: 'w-64',
|
|
185
|
-
}"
|
|
186
|
-
:modal="false"
|
|
187
|
-
>
|
|
188
|
-
<button
|
|
189
|
-
:aria-label="t('supabase.navbar.openUserMenu')"
|
|
190
|
-
class="group h-full py-1"
|
|
191
|
-
type="button"
|
|
192
|
-
>
|
|
193
|
-
<div
|
|
194
|
-
class="flex h-full items-center gap-x-2 rounded-sm rounded-l-xl pr-2 pl-0.5 group-hover:bg-elevated"
|
|
195
|
-
>
|
|
196
|
-
<UAvatar
|
|
197
|
-
:ui="{
|
|
198
|
-
root: 'rounded-xl',
|
|
199
|
-
icon: 'text-inverted text-lg',
|
|
200
|
-
}"
|
|
201
|
-
:alt="displayName"
|
|
202
|
-
size="sm"
|
|
203
|
-
icon="lucide:user"
|
|
204
|
-
class="bg-primary"
|
|
205
|
-
/>
|
|
206
|
-
|
|
207
|
-
<span
|
|
208
|
-
class="text-highlighted hidden text-sm font-semibold lg:inline"
|
|
209
|
-
>
|
|
210
|
-
{{ shortDisplayName }}
|
|
211
|
-
</span>
|
|
212
|
-
</div>
|
|
213
|
-
</button>
|
|
214
|
-
</UDropdownMenu>
|
|
215
|
-
</div>
|
|
216
|
-
</div>
|
|
217
|
-
</div>
|
|
218
|
-
</PlutoNavbarAdmin>
|
|
219
|
-
</template>
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
const route = useRoute()
|
|
3
|
-
const session = useSupabaseSession()
|
|
4
|
-
|
|
5
|
-
const shouldShowNavbar = computed(() => {
|
|
6
|
-
if (allowedUnauthenticatedPaths.includes(route.path)) {
|
|
7
|
-
return false
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
return route.path === '/admin'
|
|
11
|
-
|| route.path.startsWith('/admin/')
|
|
12
|
-
|| Boolean(session.value)
|
|
13
|
-
})
|
|
14
|
-
</script>
|
|
15
|
-
|
|
16
|
-
<template>
|
|
17
|
-
<NavbarAdmin v-if="shouldShowNavbar" />
|
|
18
|
-
</template>
|
package/app/middleware/auth.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { RouteLocationNormalizedGeneric } from 'vue-router'
|
|
2
|
-
|
|
3
|
-
export default defineNuxtRouteMiddleware(async (to, from) => {
|
|
4
|
-
const { isLoggedIn, logout, allowedUnauthenticatedPaths } = await useAuth({
|
|
5
|
-
route: to as RouteLocationNormalizedGeneric,
|
|
6
|
-
})
|
|
7
|
-
|
|
8
|
-
if (
|
|
9
|
-
isLoggedIn.value &&
|
|
10
|
-
allowedUnauthenticatedPaths.includes(to.path) &&
|
|
11
|
-
to.path !== '/admin/update-password'
|
|
12
|
-
) {
|
|
13
|
-
return navigateTo(
|
|
14
|
-
to.query.redirect
|
|
15
|
-
? decodeURIComponent(to.query.redirect as string)
|
|
16
|
-
: '/admin/home'
|
|
17
|
-
)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
!isLoggedIn.value &&
|
|
22
|
-
to.path.startsWith('/admin') &&
|
|
23
|
-
!allowedUnauthenticatedPaths.includes(to.path)
|
|
24
|
-
) {
|
|
25
|
-
if (from.query.redirect) {
|
|
26
|
-
return logout({
|
|
27
|
-
redirectTo: from.query.redirect as string,
|
|
28
|
-
showToast: true,
|
|
29
|
-
})
|
|
30
|
-
} else {
|
|
31
|
-
return logout()
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
})
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
export default defineNuxtRouteMiddleware(async (to, _from) => {
|
|
2
|
-
if (!to.path.startsWith('/admin')) {
|
|
3
|
-
return
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
try {
|
|
7
|
-
const data = await $fetch('/api/settings/first_setup')
|
|
8
|
-
|
|
9
|
-
if (!data.success) {
|
|
10
|
-
// Cannot determine setup state — send to setup page
|
|
11
|
-
if (to.path !== '/admin/setup') {
|
|
12
|
-
return navigateTo('/admin/setup')
|
|
13
|
-
}
|
|
14
|
-
return
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (data.is_first_setup) {
|
|
18
|
-
// Setup not done yet — only allow the setup page
|
|
19
|
-
if (to.path !== '/admin/setup') {
|
|
20
|
-
return navigateTo('/admin/setup')
|
|
21
|
-
}
|
|
22
|
-
} else {
|
|
23
|
-
// Setup already done — block access to setup page.
|
|
24
|
-
//
|
|
25
|
-
// Keep this redirect even after layer migrations move to
|
|
26
|
-
// /admin/migrations. The setup page prompts an unauthenticated
|
|
27
|
-
// visitor for a raw database connection string, and it is safe only
|
|
28
|
-
// because it is unreachable once setup is done. Reopening it for a
|
|
29
|
-
// routine layer addition would let an anonymous visitor submit a
|
|
30
|
-
// connection string to the server.
|
|
31
|
-
if (to.path === '/admin/setup') {
|
|
32
|
-
return navigateTo('/admin/login')
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
} catch (error) {
|
|
36
|
-
if (import.meta.dev) {
|
|
37
|
-
console.error('Error checking first setup status:', error)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (to.path !== '/admin/setup') {
|
|
41
|
-
return navigateTo('/admin/setup')
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
})
|