@nuxt-tmpl/nuxt 0.0.5
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/dist/module.d.mts +25 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +177 -0
- package/dist/runtime/components/auth/AuthGuard.vue +11 -0
- package/dist/runtime/components/auth/AuthGuard.vue.d.ts +2 -0
- package/dist/runtime/components/auth/AuthSignIn.vue +14 -0
- package/dist/runtime/components/auth/AuthSignIn.vue.d.ts +2 -0
- package/dist/runtime/components/auth/AuthSignInButton.vue +10 -0
- package/dist/runtime/components/auth/AuthSignInButton.vue.d.ts +2 -0
- package/dist/runtime/components/auth/AuthSignUp.vue +14 -0
- package/dist/runtime/components/auth/AuthSignUp.vue.d.ts +2 -0
- package/dist/runtime/components/auth/AuthSignUpButton.vue +10 -0
- package/dist/runtime/components/auth/AuthSignUpButton.vue.d.ts +2 -0
- package/dist/runtime/components/auth/AuthUserAvatar.vue +20 -0
- package/dist/runtime/components/auth/AuthUserAvatar.vue.d.ts +2 -0
- package/dist/runtime/components/auth/DefaultSignIn.vue +70 -0
- package/dist/runtime/components/auth/DefaultSignIn.vue.d.ts +2 -0
- package/dist/runtime/components/auth/DefaultSignUp.vue +81 -0
- package/dist/runtime/components/auth/DefaultSignUp.vue.d.ts +2 -0
- package/dist/runtime/composables/useAuth.d.ts +2 -0
- package/dist/runtime/composables/useAuth.js +5 -0
- package/dist/runtime/composables/useAuthForms.d.ts +9 -0
- package/dist/runtime/composables/useAuthForms.js +8 -0
- package/dist/runtime/layouts/default.vue +79 -0
- package/dist/runtime/layouts/default.vue.d.ts +2 -0
- package/dist/runtime/middleware/auth.d.ts +2 -0
- package/dist/runtime/middleware/auth.js +32 -0
- package/dist/runtime/pages/sign-in.vue +18 -0
- package/dist/runtime/pages/sign-in.vue.d.ts +2 -0
- package/dist/runtime/pages/sign-up.vue +18 -0
- package/dist/runtime/pages/sign-up.vue.d.ts +2 -0
- package/dist/runtime/plugins/auth.d.ts +6 -0
- package/dist/runtime/plugins/auth.js +10 -0
- package/dist/runtime/providers/better-auth.d.ts +2 -0
- package/dist/runtime/providers/better-auth.js +60 -0
- package/dist/runtime/providers/clerk.d.ts +7 -0
- package/dist/runtime/providers/clerk.js +112 -0
- package/dist/runtime/providers/index.d.ts +2 -0
- package/dist/runtime/providers/index.js +48 -0
- package/dist/runtime/providers/supabase.d.ts +3 -0
- package/dist/runtime/providers/supabase.js +77 -0
- package/dist/runtime/server/guards/better-auth.d.ts +9 -0
- package/dist/runtime/server/guards/better-auth.js +13 -0
- package/dist/runtime/server/guards/clerk.d.ts +3 -0
- package/dist/runtime/server/guards/clerk.js +16 -0
- package/dist/runtime/server/guards/supabase.d.ts +3 -0
- package/dist/runtime/server/guards/supabase.js +12 -0
- package/dist/runtime/server/middleware/auth.d.ts +2 -0
- package/dist/runtime/server/middleware/auth.js +26 -0
- package/dist/runtime/server/middleware/log.d.ts +2 -0
- package/dist/runtime/server/middleware/log.js +4 -0
- package/dist/runtime/server/plugins/api-response.d.ts +11 -0
- package/dist/runtime/server/plugins/api-response.js +32 -0
- package/dist/runtime/server/utils/api-response.d.ts +17 -0
- package/dist/runtime/server/utils/api-response.js +11 -0
- package/dist/runtime/server/utils/auth.d.ts +6 -0
- package/dist/runtime/server/utils/auth.js +7 -0
- package/dist/runtime/server/utils/handler.d.ts +8 -0
- package/dist/runtime/server/utils/handler.js +18 -0
- package/dist/runtime/server/utils/llm.d.ts +12 -0
- package/dist/runtime/server/utils/llm.js +35 -0
- package/dist/runtime/server/utils/supabase.d.ts +2 -0
- package/dist/runtime/server/utils/supabase.js +28 -0
- package/dist/runtime/utils/authRoute.d.ts +10 -0
- package/dist/runtime/utils/authRoute.js +10 -0
- package/dist/types.d.mts +9 -0
- package/package.json +53 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as nuxt_schema from 'nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface NuxtTmplOptions {
|
|
4
|
+
auth?: {
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
afterSignInRoute?: string;
|
|
7
|
+
guestRoutes?: string[];
|
|
8
|
+
publicRoutes?: string[];
|
|
9
|
+
};
|
|
10
|
+
apiResponse?: {
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
};
|
|
13
|
+
logging?: {
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
declare module '#app' {
|
|
18
|
+
interface PageMeta {
|
|
19
|
+
auth?: 'guest' | 'public' | 'required';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
declare const _default: nuxt_schema.NuxtModule<NuxtTmplOptions, NuxtTmplOptions, false>;
|
|
23
|
+
|
|
24
|
+
export { _default as default };
|
|
25
|
+
export type { NuxtTmplOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { defineNuxtModule, createResolver, addImports, addComponentsDir, addPlugin, addLayout, addServerHandler, addTemplate, addServerPlugin, addRouteMiddleware, extendPages, addServerImportsDir } from '@nuxt/kit';
|
|
5
|
+
|
|
6
|
+
const module$1 = defineNuxtModule({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "@nuxt-tmpl/nuxt",
|
|
9
|
+
configKey: "nuxtTmpl"
|
|
10
|
+
},
|
|
11
|
+
defaults: {
|
|
12
|
+
auth: { enabled: true, afterSignInRoute: "/dashboard" },
|
|
13
|
+
apiResponse: { enabled: true },
|
|
14
|
+
logging: { enabled: false }
|
|
15
|
+
},
|
|
16
|
+
moduleDependencies(nuxt) {
|
|
17
|
+
const provider = process.env.NUXT_PUBLIC_AUTH_PROVIDER || "";
|
|
18
|
+
if (provider !== "clerk")
|
|
19
|
+
return {};
|
|
20
|
+
const publishableKey = process.env.NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY;
|
|
21
|
+
if (!publishableKey) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"[nuxt-tmpl] NUXT_PUBLIC_AUTH_PROVIDER=clerk requires NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY to be set"
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const afterSignInRoute = nuxt.options.nuxtTmpl?.auth?.afterSignInRoute ?? "/dashboard";
|
|
27
|
+
return {
|
|
28
|
+
"@clerk/nuxt": {
|
|
29
|
+
defaults: {
|
|
30
|
+
publishableKey,
|
|
31
|
+
afterSignOutUrl: "/",
|
|
32
|
+
signInForceRedirectUrl: afterSignInRoute,
|
|
33
|
+
signInFallbackRedirectUrl: afterSignInRoute
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
async setup(options, nuxt) {
|
|
39
|
+
const { resolve } = createResolver(import.meta.url);
|
|
40
|
+
const provider = process.env.NUXT_PUBLIC_AUTH_PROVIDER || "";
|
|
41
|
+
nuxt.options.runtimeConfig.public = nuxt.options.runtimeConfig.public || {};
|
|
42
|
+
nuxt.options.runtimeConfig.public.authProvider = provider;
|
|
43
|
+
nuxt.options.runtimeConfig.public.baseUrl = process.env.BASE_URL || "";
|
|
44
|
+
nuxt.options.runtimeConfig.public.nuxtTmpl = {
|
|
45
|
+
afterSignInRoute: options.auth?.afterSignInRoute ?? "/dashboard",
|
|
46
|
+
guestRoutes: options.auth?.guestRoutes ?? [],
|
|
47
|
+
publicRoutes: options.auth?.publicRoutes ?? [],
|
|
48
|
+
...nuxt.options.runtimeConfig.public.nuxtTmpl || {}
|
|
49
|
+
};
|
|
50
|
+
if (provider === "clerk") {
|
|
51
|
+
nuxt.options.runtimeConfig.clerk = {
|
|
52
|
+
secretKey: process.env.NUXT_CLERK_SECRET_KEY,
|
|
53
|
+
...nuxt.options.runtimeConfig.clerk || {}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
addImports([
|
|
57
|
+
{
|
|
58
|
+
name: "defineApiHandler",
|
|
59
|
+
as: "defineApiHandler",
|
|
60
|
+
from: resolve("./runtime/server/utils/handler")
|
|
61
|
+
}
|
|
62
|
+
]);
|
|
63
|
+
if (options.auth?.enabled !== false) {
|
|
64
|
+
addComponentsDir({
|
|
65
|
+
path: resolve("./runtime/components/auth"),
|
|
66
|
+
prefix: "Auth"
|
|
67
|
+
});
|
|
68
|
+
addPlugin(resolve("./runtime/plugins/auth"));
|
|
69
|
+
addImports([
|
|
70
|
+
{
|
|
71
|
+
name: "useAuth",
|
|
72
|
+
as: "useAuth",
|
|
73
|
+
from: resolve("./runtime/composables/useAuth")
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "provideAuthForms",
|
|
77
|
+
as: "provideAuthForms",
|
|
78
|
+
from: resolve("./runtime/composables/useAuthForms")
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "useAuthForms",
|
|
82
|
+
as: "useAuthForms",
|
|
83
|
+
from: resolve("./runtime/composables/useAuthForms")
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "getAuthMode",
|
|
87
|
+
as: "getAuthMode",
|
|
88
|
+
from: resolve("./runtime/utils/authRoute")
|
|
89
|
+
}
|
|
90
|
+
]);
|
|
91
|
+
addLayout(
|
|
92
|
+
{ src: resolve("./runtime/layouts/default.vue") },
|
|
93
|
+
"ipa-default"
|
|
94
|
+
);
|
|
95
|
+
addServerHandler({
|
|
96
|
+
middleware: true,
|
|
97
|
+
handler: resolve("./runtime/server/middleware/auth")
|
|
98
|
+
});
|
|
99
|
+
const tmpl = addTemplate({
|
|
100
|
+
filename: "nuxt-tmpl/auth-guards.mjs",
|
|
101
|
+
write: true,
|
|
102
|
+
getContents: () => generateAuthGuards(nuxt, resolve)
|
|
103
|
+
});
|
|
104
|
+
addServerPlugin(tmpl.dst);
|
|
105
|
+
addRouteMiddleware({
|
|
106
|
+
name: "auth",
|
|
107
|
+
path: resolve("./runtime/middleware/auth"),
|
|
108
|
+
global: true
|
|
109
|
+
});
|
|
110
|
+
extendPages((pages) => {
|
|
111
|
+
const hasSignIn = pages.some((p) => p.path.startsWith("/sign-in"));
|
|
112
|
+
const hasSignUp = pages.some((p) => p.path.startsWith("/sign-up"));
|
|
113
|
+
if (!hasSignIn) {
|
|
114
|
+
pages.push({
|
|
115
|
+
name: "sign-in",
|
|
116
|
+
path: "/sign-in/:slug(.*)*",
|
|
117
|
+
file: resolve("./runtime/pages/sign-in.vue")
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (!hasSignUp) {
|
|
121
|
+
pages.push({
|
|
122
|
+
name: "sign-up",
|
|
123
|
+
path: "/sign-up/:slug(.*)*",
|
|
124
|
+
file: resolve("./runtime/pages/sign-up.vue")
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (options.apiResponse?.enabled !== false) {
|
|
130
|
+
addServerPlugin(resolve("./runtime/server/plugins/api-response"));
|
|
131
|
+
}
|
|
132
|
+
if (options.logging?.enabled) {
|
|
133
|
+
addServerHandler({
|
|
134
|
+
middleware: true,
|
|
135
|
+
handler: resolve("./runtime/server/middleware/log")
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
addServerImportsDir(resolve("./runtime/server/utils"));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
function generateAuthGuards(nuxt, resolve) {
|
|
142
|
+
const r = (path) => pathToFileURL(`${resolve(path)}.js`).href;
|
|
143
|
+
const imports = [
|
|
144
|
+
`import { registerAuthGuard } from ${JSON.stringify(r("./runtime/server/utils/auth"))}`
|
|
145
|
+
];
|
|
146
|
+
const registrations = [];
|
|
147
|
+
imports.push(
|
|
148
|
+
`import { clerkGuard } from ${JSON.stringify(r("./runtime/server/guards/clerk"))}`
|
|
149
|
+
);
|
|
150
|
+
registrations.push(` registerAuthGuard('clerk', clerkGuard)`);
|
|
151
|
+
imports.push(
|
|
152
|
+
`import { supabaseGuard } from ${JSON.stringify(r("./runtime/server/guards/supabase"))}`
|
|
153
|
+
);
|
|
154
|
+
registrations.push(` registerAuthGuard('supabase', supabaseGuard)`);
|
|
155
|
+
const sharedDir = nuxt.options.alias?.["#shared"];
|
|
156
|
+
if (sharedDir) {
|
|
157
|
+
const fileExists = [".ts", ".mjs", ".js"].some(
|
|
158
|
+
(ext) => existsSync(join(sharedDir, `utils/better-auth${ext}`))
|
|
159
|
+
);
|
|
160
|
+
if (fileExists) {
|
|
161
|
+
imports.push(
|
|
162
|
+
`import { betterAuthGuard } from ${JSON.stringify(r("./runtime/server/guards/better-auth"))}`
|
|
163
|
+
);
|
|
164
|
+
registrations.push(` registerAuthGuard('better-auth', betterAuthGuard)`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return [
|
|
168
|
+
...imports,
|
|
169
|
+
"",
|
|
170
|
+
"const plugin = (nitroApp) => {",
|
|
171
|
+
...registrations,
|
|
172
|
+
"}",
|
|
173
|
+
"export default plugin"
|
|
174
|
+
].join("\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export { module$1 as default };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useAuth } from "#imports";
|
|
3
|
+
const provider = useAuth();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<slot v-if="provider.isSignedIn.value" />
|
|
8
|
+
<slot v-else name="fallback">
|
|
9
|
+
<div>Please sign in to view this content.</div>
|
|
10
|
+
</slot>
|
|
11
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useAuth, useAuthForms } from "#imports";
|
|
4
|
+
import DefaultSignIn from "./DefaultSignIn.vue";
|
|
5
|
+
const appForms = useAuthForms();
|
|
6
|
+
const provider = useAuth();
|
|
7
|
+
const signInForm = computed(
|
|
8
|
+
() => appForms.SignInForm || provider.SignInForm || DefaultSignIn
|
|
9
|
+
);
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<component :is="signInForm" v-bind="$attrs" />
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useAuth, useAuthForms } from "#imports";
|
|
4
|
+
import DefaultSignUp from "./DefaultSignUp.vue";
|
|
5
|
+
const appForms = useAuthForms();
|
|
6
|
+
const provider = useAuth();
|
|
7
|
+
const signUpForm = computed(
|
|
8
|
+
() => appForms.SignUpForm || provider.SignUpForm || DefaultSignUp
|
|
9
|
+
);
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<component :is="signUpForm" v-bind="$attrs" />
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useAuth } from "#imports";
|
|
4
|
+
const provider = useAuth();
|
|
5
|
+
const user = computed(() => provider.user.value);
|
|
6
|
+
const initials = computed(() => {
|
|
7
|
+
const name = user.value?.name || user.value?.email || "";
|
|
8
|
+
return name ? name.charAt(0).toUpperCase() : "";
|
|
9
|
+
});
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<img v-if="user.avatarUrl" :src="user.avatarUrl" class="ipa-user-avatar ipa-user-avatar--image">
|
|
14
|
+
<span v-else class="ipa-user-avatar ipa-user-avatar--text">{{ initials
|
|
15
|
+
}}</span>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<style>
|
|
19
|
+
.ipa-user-avatar{border-radius:50%;cursor:pointer;height:32px;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:32px}.ipa-user-avatar--image{-o-object-fit:cover;object-fit:cover}.ipa-user-avatar--text{align-items:center;background:#e0e0e0;color:var(--el-text-color-primary,#555);display:inline-flex;font-size:14px;font-weight:600;justify-content:center}
|
|
20
|
+
</style>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useTranslator } from "@ipa-vue/core";
|
|
3
|
+
import { ref } from "vue";
|
|
4
|
+
import { useRouter } from "vue-router";
|
|
5
|
+
import { useAuth } from "#imports";
|
|
6
|
+
defineProps({
|
|
7
|
+
withSignUp: { type: Boolean, required: false }
|
|
8
|
+
});
|
|
9
|
+
const { signIn } = useAuth();
|
|
10
|
+
const router = useRouter();
|
|
11
|
+
const t = useTranslator();
|
|
12
|
+
const email = ref("");
|
|
13
|
+
const password = ref("");
|
|
14
|
+
const loading = ref(false);
|
|
15
|
+
const error = ref("");
|
|
16
|
+
async function handleSubmit() {
|
|
17
|
+
loading.value = true;
|
|
18
|
+
error.value = "";
|
|
19
|
+
const result = await signIn({ email: email.value, password: password.value });
|
|
20
|
+
if (!result.success)
|
|
21
|
+
error.value = result.error?.message ?? "";
|
|
22
|
+
loading.value = false;
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<form class="ntm-form" @submit.prevent="handleSubmit">
|
|
28
|
+
<p v-if="error" class="ntm-form__error" role="alert">
|
|
29
|
+
{{ error }}
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<div class="ntm-form__field">
|
|
33
|
+
<input
|
|
34
|
+
v-model="email"
|
|
35
|
+
class="ntm-form__input"
|
|
36
|
+
type="email"
|
|
37
|
+
:placeholder="t('common.email') || 'Email'"
|
|
38
|
+
autocomplete="email"
|
|
39
|
+
required
|
|
40
|
+
>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="ntm-form__field">
|
|
44
|
+
<input
|
|
45
|
+
v-model="password"
|
|
46
|
+
class="ntm-form__input"
|
|
47
|
+
type="password"
|
|
48
|
+
:placeholder="t('common.password') || 'Password'"
|
|
49
|
+
autocomplete="current-password"
|
|
50
|
+
required
|
|
51
|
+
>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<button type="submit" class="ntm-form__submit" :disabled="loading">
|
|
55
|
+
<span v-if="loading" class="ntm-form__spinner" aria-hidden="true" />
|
|
56
|
+
{{ t("signIn") || "Sign In" }}
|
|
57
|
+
</button>
|
|
58
|
+
|
|
59
|
+
<p v-if="withSignUp" class="ntm-form__switch">
|
|
60
|
+
{{ t("auth.noAccount", "") || "Don't have an account?" }}
|
|
61
|
+
<a class="ntm-form__link" @click.prevent="router.push('/sign-up')">
|
|
62
|
+
{{ t("signUp") || "Sign Up" }}
|
|
63
|
+
</a>
|
|
64
|
+
</p>
|
|
65
|
+
</form>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<style scoped>
|
|
69
|
+
.ntm-form{display:flex;flex-direction:column;gap:14px;width:100%}.ntm-form__input{background:var(--el-fill-color-blank,#fff);border:1px solid var(--el-border-color,#dcdfe6);border-radius:var(--el-border-radius-base,4px);box-sizing:border-box;color:var(--el-text-color-primary,#303133);font-size:14px;height:38px;outline:none;padding:0 12px;transition:border-color .2s;width:100%}.ntm-form__input::-moz-placeholder{color:var(--el-text-color-placeholder,#a8abb2)}.ntm-form__input::placeholder{color:var(--el-text-color-placeholder,#a8abb2)}.ntm-form__input:focus{border-color:var(--el-color-primary,#409eff)}.ntm-form__submit{align-items:center;background:var(--el-color-primary,#409eff);border:none;border-radius:var(--el-border-radius-base,4px);color:#fff;cursor:pointer;display:flex;font-size:14px;font-weight:500;gap:8px;height:38px;justify-content:center;transition:opacity .2s;width:100%}.ntm-form__submit:hover:not(:disabled){opacity:.85}.ntm-form__submit:disabled{cursor:not-allowed;opacity:.6}.ntm-form__spinner{animation:ntm-spin .6s linear infinite;border:2px solid hsla(0,0%,100%,.4);border-radius:50%;border-top-color:#fff;flex-shrink:0;height:14px;width:14px}@keyframes ntm-spin{to{transform:rotate(1turn)}}.ntm-form__error{background:var(--el-color-danger-light-9,#fef0f0);border-radius:var(--el-border-radius-base,4px);color:var(--el-color-danger,#f56c6c);font-size:13px;margin:0;padding:8px 12px}.ntm-form__switch{color:var(--el-text-color-secondary,#909399);font-size:13px;margin:0;text-align:center}.ntm-form__link{color:var(--el-color-primary,#409eff);cursor:pointer;text-decoration:none}.ntm-form__link:hover{opacity:.8}@media (prefers-color-scheme:dark){.ntm-form__input{background:var(--el-fill-color-blank,#1d1e1f);border-color:var(--el-border-color,#4c4d4f);color:var(--el-text-color-primary,#e5eaf3)}.ntm-form__input::-moz-placeholder{color:var(--el-text-color-placeholder,#606266)}.ntm-form__input::placeholder{color:var(--el-text-color-placeholder,#606266)}.ntm-form__error{background:var(--el-color-danger-light-9,#2b1d1d)}}
|
|
70
|
+
</style>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useTranslator } from "@ipa-vue/core";
|
|
3
|
+
import { ref } from "vue";
|
|
4
|
+
import { useRouter } from "vue-router";
|
|
5
|
+
import { useAuth } from "#imports";
|
|
6
|
+
defineProps({
|
|
7
|
+
withSignIn: { type: Boolean, required: false }
|
|
8
|
+
});
|
|
9
|
+
const { signUp } = useAuth();
|
|
10
|
+
const router = useRouter();
|
|
11
|
+
const t = useTranslator();
|
|
12
|
+
const name = ref("");
|
|
13
|
+
const email = ref("");
|
|
14
|
+
const password = ref("");
|
|
15
|
+
const loading = ref(false);
|
|
16
|
+
const error = ref("");
|
|
17
|
+
async function handleSubmit() {
|
|
18
|
+
loading.value = true;
|
|
19
|
+
error.value = "";
|
|
20
|
+
const result = await signUp({ name: name.value, email: email.value, password: password.value });
|
|
21
|
+
if (!result.success)
|
|
22
|
+
error.value = result.error?.message ?? "";
|
|
23
|
+
loading.value = false;
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<form class="ntm-form" @submit.prevent="handleSubmit">
|
|
29
|
+
<p v-if="error" class="ntm-form__error" role="alert">
|
|
30
|
+
{{ error }}
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
<div class="ntm-form__field">
|
|
34
|
+
<input
|
|
35
|
+
v-model="name"
|
|
36
|
+
class="ntm-form__input"
|
|
37
|
+
type="text"
|
|
38
|
+
:placeholder="t('common.name') || 'Name'"
|
|
39
|
+
autocomplete="name"
|
|
40
|
+
>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="ntm-form__field">
|
|
44
|
+
<input
|
|
45
|
+
v-model="email"
|
|
46
|
+
class="ntm-form__input"
|
|
47
|
+
type="email"
|
|
48
|
+
:placeholder="t('common.email') || 'Email'"
|
|
49
|
+
autocomplete="email"
|
|
50
|
+
required
|
|
51
|
+
>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="ntm-form__field">
|
|
55
|
+
<input
|
|
56
|
+
v-model="password"
|
|
57
|
+
class="ntm-form__input"
|
|
58
|
+
type="password"
|
|
59
|
+
:placeholder="t('common.password') || 'Password'"
|
|
60
|
+
autocomplete="new-password"
|
|
61
|
+
required
|
|
62
|
+
>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<button type="submit" class="ntm-form__submit" :disabled="loading">
|
|
66
|
+
<span v-if="loading" class="ntm-form__spinner" aria-hidden="true" />
|
|
67
|
+
{{ t("signUp") || "Sign Up" }}
|
|
68
|
+
</button>
|
|
69
|
+
|
|
70
|
+
<p v-if="withSignIn" class="ntm-form__switch">
|
|
71
|
+
{{ t("auth.hasAccount") || "Already have an account?" }}
|
|
72
|
+
<a class="ntm-form__link" @click.prevent="router.push('/sign-in')">
|
|
73
|
+
{{ t("signIn") || "Sign In" }}
|
|
74
|
+
</a>
|
|
75
|
+
</p>
|
|
76
|
+
</form>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<style scoped>
|
|
80
|
+
.ntm-form{display:flex;flex-direction:column;gap:14px;width:100%}.ntm-form__input{background:var(--el-fill-color-blank,#fff);border:1px solid var(--el-border-color,#dcdfe6);border-radius:var(--el-border-radius-base,4px);box-sizing:border-box;color:var(--el-text-color-primary,#303133);font-size:14px;height:38px;outline:none;padding:0 12px;transition:border-color .2s;width:100%}.ntm-form__input::-moz-placeholder{color:var(--el-text-color-placeholder,#a8abb2)}.ntm-form__input::placeholder{color:var(--el-text-color-placeholder,#a8abb2)}.ntm-form__input:focus{border-color:var(--el-color-primary,#409eff)}.ntm-form__submit{align-items:center;background:var(--el-color-primary,#409eff);border:none;border-radius:var(--el-border-radius-base,4px);color:#fff;cursor:pointer;display:flex;font-size:14px;font-weight:500;gap:8px;height:38px;justify-content:center;transition:opacity .2s;width:100%}.ntm-form__submit:hover:not(:disabled){opacity:.85}.ntm-form__submit:disabled{cursor:not-allowed;opacity:.6}.ntm-form__spinner{animation:ntm-spin .6s linear infinite;border:2px solid hsla(0,0%,100%,.4);border-radius:50%;border-top-color:#fff;flex-shrink:0;height:14px;width:14px}@keyframes ntm-spin{to{transform:rotate(1turn)}}.ntm-form__error{background:var(--el-color-danger-light-9,#fef0f0);border-radius:var(--el-border-radius-base,4px);color:var(--el-color-danger,#f56c6c);font-size:13px;margin:0;padding:8px 12px}.ntm-form__switch{color:var(--el-text-color-secondary,#909399);font-size:13px;margin:0;text-align:center}.ntm-form__link{color:var(--el-color-primary,#409eff);cursor:pointer;text-decoration:none}.ntm-form__link:hover{opacity:.8}@media (prefers-color-scheme:dark){.ntm-form__input{background:var(--el-fill-color-blank,#1d1e1f);border-color:var(--el-border-color,#4c4d4f);color:var(--el-text-color-primary,#e5eaf3)}.ntm-form__input::-moz-placeholder{color:var(--el-text-color-placeholder,#606266)}.ntm-form__input::placeholder{color:var(--el-text-color-placeholder,#606266)}.ntm-form__error{background:var(--el-color-danger-light-9,#2b1d1d)}}
|
|
81
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Component } from "vue";
|
|
2
|
+
interface AuthForms {
|
|
3
|
+
SignInForm?: Component;
|
|
4
|
+
SignUpForm?: Component;
|
|
5
|
+
}
|
|
6
|
+
/** Call inside a component's `<script setup>` (e.g. `app.vue`) to override the default auth form components. */
|
|
7
|
+
export declare function provideAuthForms(forms: AuthForms): void;
|
|
8
|
+
export declare function useAuthForms(): AuthForms;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useTranslator } from "@ipa-vue/core";
|
|
3
|
+
import { useRuntimeConfig } from "nuxt/app";
|
|
4
|
+
import { computed, watch } from "vue";
|
|
5
|
+
import { useRoute, useRouter } from "vue-router";
|
|
6
|
+
import { getAuthMode, useAuth } from "#imports";
|
|
7
|
+
const t = useTranslator();
|
|
8
|
+
const provider = useAuth();
|
|
9
|
+
const user = computed(() => provider.user.value);
|
|
10
|
+
const route = useRoute();
|
|
11
|
+
const router = useRouter();
|
|
12
|
+
const config = useRuntimeConfig();
|
|
13
|
+
const afterSignInRoute = config.public?.nuxtTmpl?.afterSignInRoute ?? "/dashboard";
|
|
14
|
+
const guestRoutes = config.public?.nuxtTmpl?.guestRoutes ?? [];
|
|
15
|
+
const publicRoutes = config.public?.nuxtTmpl?.publicRoutes ?? [];
|
|
16
|
+
const authMode = computed(
|
|
17
|
+
() => getAuthMode(route.path, route.meta, guestRoutes, publicRoutes)
|
|
18
|
+
);
|
|
19
|
+
watch(
|
|
20
|
+
[() => provider.isLoaded.value, () => provider.isSignedIn.value, authMode],
|
|
21
|
+
([loaded, signedIn, mode]) => {
|
|
22
|
+
if (!loaded || provider.id === "none")
|
|
23
|
+
return;
|
|
24
|
+
if (signedIn && mode === "guest") {
|
|
25
|
+
router.push(afterSignInRoute);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!signedIn && mode === "required") {
|
|
29
|
+
router.push("/");
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{ immediate: true }
|
|
33
|
+
);
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<div class="flex flex-col w-full h-full">
|
|
38
|
+
<div style="border-bottom: 1px solid var(--el-border-color)">
|
|
39
|
+
<div class="mx-auto px-4 sm:px-6 lg:px-8">
|
|
40
|
+
<div class="flex justify-between items-center h-10">
|
|
41
|
+
<slot name="app-name">
|
|
42
|
+
<el-link type="primary" href="/" class="text-lg font-bold">
|
|
43
|
+
{{ t("appName") }}
|
|
44
|
+
</el-link>
|
|
45
|
+
</slot>
|
|
46
|
+
<slot name="nav" />
|
|
47
|
+
<nav class="flex items-center space-x-4">
|
|
48
|
+
<slot name="header-actions" />
|
|
49
|
+
<template v-if="provider.id !== 'none'">
|
|
50
|
+
<template v-if="!provider.isSignedIn.value">
|
|
51
|
+
<AuthSignInButton>
|
|
52
|
+
<el-button size="small" type="primary">
|
|
53
|
+
{{ t("signIn") }}
|
|
54
|
+
</el-button>
|
|
55
|
+
</AuthSignInButton>
|
|
56
|
+
<AuthSignUpButton>
|
|
57
|
+
<el-button size="small" type="primary">
|
|
58
|
+
{{ t("signUp") }}
|
|
59
|
+
</el-button>
|
|
60
|
+
</AuthSignUpButton>
|
|
61
|
+
</template>
|
|
62
|
+
<slot v-else name="user-avatar" :user="user" :provider="provider">
|
|
63
|
+
<AuthUserAvatar @click="provider.openUserProfile?.()" />
|
|
64
|
+
</slot>
|
|
65
|
+
</template>
|
|
66
|
+
</nav>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<slot name="content">
|
|
71
|
+
<div class="flex-1 flex overflow-hidden">
|
|
72
|
+
<slot name="sidebar" />
|
|
73
|
+
<div class="flex-1 overflow-auto">
|
|
74
|
+
<slot />
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</slot>
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineNuxtRouteMiddleware, navigateTo, useNuxtApp, useRuntimeConfig } from "nuxt/app";
|
|
2
|
+
import { watch } from "vue";
|
|
3
|
+
import { getAuthMode } from "#imports";
|
|
4
|
+
export default defineNuxtRouteMiddleware(async (to, _from) => {
|
|
5
|
+
const config = useRuntimeConfig();
|
|
6
|
+
const providerId = config.public?.authProvider;
|
|
7
|
+
if (!providerId || providerId === "none")
|
|
8
|
+
return;
|
|
9
|
+
const guestRoutes = config.public?.nuxtTmpl?.guestRoutes ?? [];
|
|
10
|
+
const publicRoutes = config.public?.nuxtTmpl?.publicRoutes ?? [];
|
|
11
|
+
const mode = getAuthMode(to.path, to.meta, guestRoutes, publicRoutes);
|
|
12
|
+
if (mode === "public")
|
|
13
|
+
return;
|
|
14
|
+
const { $auth } = useNuxtApp();
|
|
15
|
+
if (!$auth.isLoaded.value) {
|
|
16
|
+
await new Promise((resolve) => {
|
|
17
|
+
const stop = watch(
|
|
18
|
+
() => $auth.isLoaded.value,
|
|
19
|
+
(loaded) => {
|
|
20
|
+
if (loaded) {
|
|
21
|
+
stop();
|
|
22
|
+
resolve();
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{ immediate: true }
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (!$auth.isSignedIn.value && mode === "required" && _from.path !== "/") {
|
|
30
|
+
return navigateTo("/");
|
|
31
|
+
}
|
|
32
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="auth-page">
|
|
3
|
+
<div class="auth-card">
|
|
4
|
+
<h2 class="auth-card__title">
|
|
5
|
+
{{ $t?.("signIn") ?? "Sign In" }}
|
|
6
|
+
</h2>
|
|
7
|
+
<AuthSignIn with-sign-up />
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup>
|
|
13
|
+
definePageMeta({ tab: false });
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<style scoped>
|
|
17
|
+
.auth-page{align-items:center;display:flex;height:100%;justify-content:center}.auth-card{background:var(--el-bg-color);border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.08);display:flex;flex-direction:column;gap:24px;padding:40px;width:400px}.auth-card__title{color:var(--el-text-color-primary);font-size:22px;font-weight:600;margin:0;text-align:center}
|
|
18
|
+
</style>
|