@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,18 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="auth-page">
|
|
3
|
+
<div class="auth-card">
|
|
4
|
+
<h2 class="auth-card__title">
|
|
5
|
+
{{ $t?.("signUp") ?? "Sign Up" }}
|
|
6
|
+
</h2>
|
|
7
|
+
<AuthSignUp with-sign-in />
|
|
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>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useRuntimeConfig, navigateTo } from "nuxt/app";
|
|
2
|
+
import { negativeApiResponse, positiveApiResponse, toApiError } from "@ipa-schema/api";
|
|
3
|
+
import { createAuthClient } from "better-auth/vue";
|
|
4
|
+
import { computed } from "vue";
|
|
5
|
+
export function createBetterAuthProvider() {
|
|
6
|
+
const config = useRuntimeConfig();
|
|
7
|
+
const baseURL = config.public.betterAuth?.baseUrl || config.public.baseUrl || "";
|
|
8
|
+
const client = createAuthClient({ baseURL });
|
|
9
|
+
const sessionRef = client.useSession();
|
|
10
|
+
const sessionData = computed(() => sessionRef.value.data ?? null);
|
|
11
|
+
const isPending = computed(() => sessionRef.value.isPending ?? true);
|
|
12
|
+
const user = computed(() => {
|
|
13
|
+
const s = sessionData.value;
|
|
14
|
+
if (!s?.user)
|
|
15
|
+
return null;
|
|
16
|
+
const u = s.user;
|
|
17
|
+
return {
|
|
18
|
+
id: u.id,
|
|
19
|
+
email: u.email,
|
|
20
|
+
name: u.name ?? "",
|
|
21
|
+
avatarUrl: u.image ?? null,
|
|
22
|
+
createdAt: new Date(u.createdAt ?? Date.now())
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
const isLoaded = computed(() => !isPending.value);
|
|
26
|
+
const isSignedIn = computed(() => !!sessionData.value);
|
|
27
|
+
return {
|
|
28
|
+
id: "better-auth",
|
|
29
|
+
user,
|
|
30
|
+
isLoaded,
|
|
31
|
+
isSignedIn,
|
|
32
|
+
async signIn(data) {
|
|
33
|
+
const { error } = await client.signIn.email(data);
|
|
34
|
+
if (error)
|
|
35
|
+
return negativeApiResponse(toApiError(error));
|
|
36
|
+
return positiveApiResponse({});
|
|
37
|
+
},
|
|
38
|
+
async signUp(data) {
|
|
39
|
+
const { error } = await client.signUp.email({ ...data, name: data.name ?? "" });
|
|
40
|
+
if (error)
|
|
41
|
+
return negativeApiResponse(toApiError(error));
|
|
42
|
+
return positiveApiResponse({});
|
|
43
|
+
},
|
|
44
|
+
async signOut() {
|
|
45
|
+
await client.signOut();
|
|
46
|
+
},
|
|
47
|
+
async signInWithOAuth(provider) {
|
|
48
|
+
await client.signIn.social({ provider });
|
|
49
|
+
},
|
|
50
|
+
openSignIn: async () => {
|
|
51
|
+
await navigateTo("/sign-in");
|
|
52
|
+
},
|
|
53
|
+
openSignUp: async () => {
|
|
54
|
+
await navigateTo("/sign-up");
|
|
55
|
+
},
|
|
56
|
+
openUserProfile: async () => {
|
|
57
|
+
await navigateTo("/profile");
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useRuntimeConfig } from "nuxt/app";
|
|
2
|
+
import { negativeApiResponse, positiveApiResponse, toApiError } from "@ipa-schema/api";
|
|
3
|
+
import { computed, shallowRef } from "vue";
|
|
4
|
+
function toUser(u) {
|
|
5
|
+
if (!u)
|
|
6
|
+
return null;
|
|
7
|
+
return {
|
|
8
|
+
id: u.id,
|
|
9
|
+
email: u.primaryEmailAddress?.emailAddress ?? "",
|
|
10
|
+
name: u.fullName ?? u.username ?? "",
|
|
11
|
+
avatarUrl: u.imageUrl ?? null,
|
|
12
|
+
createdAt: u.createdAt
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function waitForClerk(timeoutMs = 1e4) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
if (window.Clerk?.loaded) {
|
|
18
|
+
resolve(window.Clerk);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const start = Date.now();
|
|
22
|
+
const interval = setInterval(() => {
|
|
23
|
+
if (window.Clerk?.loaded) {
|
|
24
|
+
clearInterval(interval);
|
|
25
|
+
resolve(window.Clerk);
|
|
26
|
+
} else if (Date.now() - start > timeoutMs) {
|
|
27
|
+
clearInterval(interval);
|
|
28
|
+
const pk = useRuntimeConfig().public?.clerk?.publishableKey;
|
|
29
|
+
const hint = !pk ? "publishableKey is missing \u2014 check NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY and that @clerk/nuxt is registered" : !window.Clerk ? "@clerk/nuxt plugin did not run \u2014 check that the module is registered before @nuxt-tmpl/nuxt" : "Clerk script loaded but did not finish initializing \u2014 check network/CSP";
|
|
30
|
+
reject(new Error(`Clerk failed to load within ${timeoutMs}ms (${hint})`));
|
|
31
|
+
}
|
|
32
|
+
}, 50);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function createClerkProvider() {
|
|
36
|
+
const user = shallowRef(null);
|
|
37
|
+
const isLoaded = shallowRef(false);
|
|
38
|
+
const isSignedIn = shallowRef(false);
|
|
39
|
+
if (import.meta.client) {
|
|
40
|
+
waitForClerk().then((clerk) => {
|
|
41
|
+
user.value = toUser(clerk.user);
|
|
42
|
+
isSignedIn.value = !!clerk.user;
|
|
43
|
+
isLoaded.value = true;
|
|
44
|
+
clerk.addListener((payload) => {
|
|
45
|
+
user.value = toUser(payload.user);
|
|
46
|
+
isSignedIn.value = !!payload.user;
|
|
47
|
+
});
|
|
48
|
+
}).catch((err) => {
|
|
49
|
+
console.error("[nuxt-tmpl] Clerk provider:", err);
|
|
50
|
+
isLoaded.value = true;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
id: "clerk",
|
|
55
|
+
user: computed(() => user.value),
|
|
56
|
+
isLoaded: computed(() => isLoaded.value),
|
|
57
|
+
isSignedIn: computed(() => isSignedIn.value),
|
|
58
|
+
async signIn(data) {
|
|
59
|
+
const clerk = window.Clerk;
|
|
60
|
+
if (!clerk?.client) {
|
|
61
|
+
return negativeApiResponse(toApiError("Clerk not loaded"));
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const result = await clerk.client.signIn.create({
|
|
65
|
+
identifier: data.email,
|
|
66
|
+
password: data.password
|
|
67
|
+
});
|
|
68
|
+
if (result.status === "complete") {
|
|
69
|
+
await clerk.setActive({ session: result.createdSessionId });
|
|
70
|
+
return positiveApiResponse({});
|
|
71
|
+
}
|
|
72
|
+
return negativeApiResponse(toApiError(result.status || "Sign in incomplete"));
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return negativeApiResponse(toApiError(e?.errors?.[0]?.message ?? e?.message ?? "Sign in failed"));
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
async signUp(data) {
|
|
78
|
+
const clerk = window.Clerk;
|
|
79
|
+
if (!clerk?.client) {
|
|
80
|
+
return negativeApiResponse(toApiError("Clerk not loaded"));
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const result = await clerk.client.signUp.create({
|
|
84
|
+
emailAddress: data.email,
|
|
85
|
+
password: data.password
|
|
86
|
+
});
|
|
87
|
+
if (result.status === "complete") {
|
|
88
|
+
await clerk.setActive({ session: result.createdSessionId });
|
|
89
|
+
return positiveApiResponse({});
|
|
90
|
+
}
|
|
91
|
+
return negativeApiResponse(toApiError(result.status || "Sign up incomplete"));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
return negativeApiResponse(toApiError(e?.errors?.[0]?.message ?? e?.message ?? "Sign up failed"));
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
async signOut() {
|
|
97
|
+
await window.Clerk?.signOut();
|
|
98
|
+
},
|
|
99
|
+
async signInWithOAuth(_provider) {
|
|
100
|
+
window.Clerk?.openSignIn();
|
|
101
|
+
},
|
|
102
|
+
async openSignIn() {
|
|
103
|
+
window.Clerk?.openSignIn();
|
|
104
|
+
},
|
|
105
|
+
async openSignUp() {
|
|
106
|
+
window.Clerk?.openSignUp();
|
|
107
|
+
},
|
|
108
|
+
async openUserProfile() {
|
|
109
|
+
window.Clerk?.openUserProfile();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useRuntimeConfig } from "nuxt/app";
|
|
2
|
+
import { negativeApiResponse, toApiError } from "@ipa-schema/api";
|
|
3
|
+
import { ref } from "vue";
|
|
4
|
+
function resolveAuthProvider() {
|
|
5
|
+
const config = useRuntimeConfig();
|
|
6
|
+
return config.public?.authProvider || "";
|
|
7
|
+
}
|
|
8
|
+
export async function createAuthProvider() {
|
|
9
|
+
const provider = resolveAuthProvider();
|
|
10
|
+
try {
|
|
11
|
+
if (provider === "clerk") {
|
|
12
|
+
const { createClerkProvider } = await import("./clerk.js");
|
|
13
|
+
return createClerkProvider();
|
|
14
|
+
}
|
|
15
|
+
if (provider === "better-auth") {
|
|
16
|
+
const { createBetterAuthProvider } = await import("./better-auth.js");
|
|
17
|
+
return createBetterAuthProvider();
|
|
18
|
+
}
|
|
19
|
+
if (provider === "supabase") {
|
|
20
|
+
const { createSupabaseProvider } = await import("./supabase.js");
|
|
21
|
+
return createSupabaseProvider();
|
|
22
|
+
}
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.error(`[nuxt-tmpl] Failed to initialize auth provider "${provider}":`, e);
|
|
25
|
+
if (import.meta.dev) {
|
|
26
|
+
console.warn(`[nuxt-tmpl] Falling back to noop provider. Check that "${provider}" is installed and configured correctly.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return createNoopProvider();
|
|
30
|
+
}
|
|
31
|
+
function createNoopProvider() {
|
|
32
|
+
return {
|
|
33
|
+
id: "none",
|
|
34
|
+
user: ref(null),
|
|
35
|
+
isLoaded: ref(true),
|
|
36
|
+
isSignedIn: ref(false),
|
|
37
|
+
async signIn() {
|
|
38
|
+
return negativeApiResponse(toApiError("No auth provider enabled"));
|
|
39
|
+
},
|
|
40
|
+
async signUp() {
|
|
41
|
+
return negativeApiResponse(toApiError("No auth provider enabled"));
|
|
42
|
+
},
|
|
43
|
+
async signOut() {
|
|
44
|
+
},
|
|
45
|
+
async signInWithOAuth() {
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useRuntimeConfig, navigateTo } from "nuxt/app";
|
|
2
|
+
import { negativeApiResponse, positiveApiResponse, toApiError } from "@ipa-schema/api";
|
|
3
|
+
import { createBrowserClient } from "@supabase/ssr";
|
|
4
|
+
import { computed, ref } from "vue";
|
|
5
|
+
export function toUser(sbUser) {
|
|
6
|
+
if (!sbUser)
|
|
7
|
+
return null;
|
|
8
|
+
return {
|
|
9
|
+
id: sbUser.id,
|
|
10
|
+
email: sbUser.email ?? "",
|
|
11
|
+
name: sbUser.user_metadata?.name ?? sbUser.user_metadata?.full_name ?? "",
|
|
12
|
+
avatarUrl: sbUser.user_metadata?.avatar_url ?? sbUser.user_metadata?.picture ?? null,
|
|
13
|
+
createdAt: new Date(sbUser.created_at)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function createSupabaseProvider() {
|
|
17
|
+
const config = useRuntimeConfig();
|
|
18
|
+
const supabaseUrl = config.public.supabase?.url;
|
|
19
|
+
const supabaseAnonKey = config.public.supabase?.anonKey;
|
|
20
|
+
const dbSchema = config.public.supabase?.dbSchema;
|
|
21
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
22
|
+
throw new Error("Missing NUXT_PUBLIC_SUPABASE_URL or NUXT_PUBLIC_SUPABASE_ANON_KEY");
|
|
23
|
+
}
|
|
24
|
+
const clientOptions = {};
|
|
25
|
+
if (dbSchema) {
|
|
26
|
+
clientOptions.db = { schema: dbSchema };
|
|
27
|
+
}
|
|
28
|
+
const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey, clientOptions);
|
|
29
|
+
const user = ref(null);
|
|
30
|
+
const isLoaded = ref(false);
|
|
31
|
+
const isSignedIn = computed(() => !!user.value);
|
|
32
|
+
supabase.auth.getSession().then(({ data: { session } }) => {
|
|
33
|
+
user.value = toUser(session?.user ?? null);
|
|
34
|
+
isLoaded.value = true;
|
|
35
|
+
});
|
|
36
|
+
supabase.auth.onAuthStateChange((_event, session) => {
|
|
37
|
+
user.value = toUser(session?.user ?? null);
|
|
38
|
+
isLoaded.value = true;
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
id: "supabase",
|
|
42
|
+
user,
|
|
43
|
+
isLoaded,
|
|
44
|
+
isSignedIn,
|
|
45
|
+
async signIn(data) {
|
|
46
|
+
const { error } = await supabase.auth.signInWithPassword({ email: data.email, password: data.password });
|
|
47
|
+
if (error)
|
|
48
|
+
return negativeApiResponse(toApiError(error));
|
|
49
|
+
return positiveApiResponse({});
|
|
50
|
+
},
|
|
51
|
+
async signUp(data) {
|
|
52
|
+
const { error } = await supabase.auth.signUp({
|
|
53
|
+
email: data.email,
|
|
54
|
+
password: data.password,
|
|
55
|
+
options: { data: { name: data.name } }
|
|
56
|
+
});
|
|
57
|
+
if (error)
|
|
58
|
+
return negativeApiResponse(toApiError(error));
|
|
59
|
+
return positiveApiResponse({});
|
|
60
|
+
},
|
|
61
|
+
async signOut() {
|
|
62
|
+
await supabase.auth.signOut();
|
|
63
|
+
},
|
|
64
|
+
async signInWithOAuth(provider) {
|
|
65
|
+
await supabase.auth.signInWithOAuth({ provider });
|
|
66
|
+
},
|
|
67
|
+
openSignIn: async () => {
|
|
68
|
+
await navigateTo("/sign-in");
|
|
69
|
+
},
|
|
70
|
+
openSignUp: async () => {
|
|
71
|
+
await navigateTo("/sign-up");
|
|
72
|
+
},
|
|
73
|
+
openUserProfile: async () => {
|
|
74
|
+
await navigateTo("/profile");
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AuthGuard } from '../utils/auth.js';
|
|
2
|
+
/**
|
|
3
|
+
* better-auth AuthGuard
|
|
4
|
+
*
|
|
5
|
+
* 注意:此文件仅在应用包含 shared/utils/better-auth 时
|
|
6
|
+
* 才会被生成的 nuxt-tmpl-auth-guards.mjs 引入,因此不存在的应用
|
|
7
|
+
* (Clerk/Supabase)不会触发 Nitro 对 #shared 别名的静态解析。
|
|
8
|
+
*/
|
|
9
|
+
export declare const betterAuthGuard: AuthGuard;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const betterAuthGuard = {
|
|
2
|
+
async validate(event) {
|
|
3
|
+
try {
|
|
4
|
+
const { auth } = await import("#shared/utils/better-auth");
|
|
5
|
+
const session = await auth.api.getSession({
|
|
6
|
+
headers: event.node.req.headers
|
|
7
|
+
});
|
|
8
|
+
return !!session;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const clerkGuard = {
|
|
2
|
+
async validate(event) {
|
|
3
|
+
try {
|
|
4
|
+
const { clerkMiddleware } = await import("@clerk/nuxt/server");
|
|
5
|
+
let authenticated = false;
|
|
6
|
+
await clerkMiddleware((e) => {
|
|
7
|
+
const auth = e.context.auth();
|
|
8
|
+
if (auth?.isAuthenticated)
|
|
9
|
+
authenticated = true;
|
|
10
|
+
})(event);
|
|
11
|
+
return authenticated;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const supabaseGuard = {
|
|
2
|
+
async validate(event) {
|
|
3
|
+
try {
|
|
4
|
+
const { createServerSupabaseClient } = await import("../utils/supabase.js");
|
|
5
|
+
const supabase = createServerSupabaseClient(event);
|
|
6
|
+
const { data: { session } } = await supabase.auth.getSession();
|
|
7
|
+
return !!session;
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { negativeApiResponse } from "@ipa-schema/api";
|
|
2
|
+
import { isSafeRoute } from "@nuxt-tmpl/core";
|
|
3
|
+
import { defineEventHandler, sendRedirect, setResponseStatus } from "h3";
|
|
4
|
+
import { useRuntimeConfig } from "nitropack/runtime";
|
|
5
|
+
import { getAuthGuard } from "../utils/auth.js";
|
|
6
|
+
export default defineEventHandler(async (event) => {
|
|
7
|
+
const config = useRuntimeConfig();
|
|
8
|
+
const providerId = config.public?.authProvider;
|
|
9
|
+
if (!providerId || providerId === "none")
|
|
10
|
+
return;
|
|
11
|
+
const guestRoutes = config.public?.nuxtTmpl?.guestRoutes || [];
|
|
12
|
+
const publicRoutes = config.public?.nuxtTmpl?.publicRoutes || [];
|
|
13
|
+
if (isSafeRoute(event.path, guestRoutes, publicRoutes))
|
|
14
|
+
return;
|
|
15
|
+
const guard = getAuthGuard(providerId);
|
|
16
|
+
if (!guard)
|
|
17
|
+
return;
|
|
18
|
+
const authenticated = await guard.validate(event);
|
|
19
|
+
if (authenticated)
|
|
20
|
+
return;
|
|
21
|
+
if (event.path.startsWith("/api/")) {
|
|
22
|
+
setResponseStatus(event, 401);
|
|
23
|
+
return negativeApiResponse({ code: 401, message: "unauthorized" });
|
|
24
|
+
}
|
|
25
|
+
return sendRedirect(event, "/");
|
|
26
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API 响应兜底插件
|
|
3
|
+
*
|
|
4
|
+
* 通过 Nitro 的 beforeResponse 钩子在响应发送前拦截:
|
|
5
|
+
* - 已被 defineApiHandler 包装 → 直接放行
|
|
6
|
+
* - HTML 响应(SPA fallback)→ 包装为 404 错误
|
|
7
|
+
* - Error 实例或状态码 >= 400 → 包装为错误响应
|
|
8
|
+
* - 其他成功响应 → 包装为成功响应
|
|
9
|
+
*/
|
|
10
|
+
declare const _default: any;
|
|
11
|
+
export default _default;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineNitroPlugin } from "nitropack/runtime/plugin";
|
|
2
|
+
import { negativeApiResponse, positiveApiResponse } from "@ipa-schema/api";
|
|
3
|
+
import { getResponseStatus, send, setResponseStatus } from "h3";
|
|
4
|
+
import {
|
|
5
|
+
isAlreadyWrappedResponse,
|
|
6
|
+
isApiPath,
|
|
7
|
+
isHtmlResponse
|
|
8
|
+
} from "../utils/api-response.js";
|
|
9
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
10
|
+
nitroApp.hooks.hook("beforeResponse", async (event, { body }) => {
|
|
11
|
+
if (!isApiPath(event.path))
|
|
12
|
+
return;
|
|
13
|
+
if (event.context._skipResponseWrap)
|
|
14
|
+
return;
|
|
15
|
+
if (isAlreadyWrappedResponse(body))
|
|
16
|
+
return;
|
|
17
|
+
const statusCode = getResponseStatus(event);
|
|
18
|
+
const isHtml = isHtmlResponse(body);
|
|
19
|
+
let wrappedBody;
|
|
20
|
+
if (isHtml || body instanceof Error || statusCode >= 400) {
|
|
21
|
+
const code = isHtml ? 404 : statusCode;
|
|
22
|
+
wrappedBody = negativeApiResponse({
|
|
23
|
+
code,
|
|
24
|
+
message: isHtml ? "Not Found" : body instanceof Error ? body.message : "Error"
|
|
25
|
+
});
|
|
26
|
+
setResponseStatus(event, code);
|
|
27
|
+
} else {
|
|
28
|
+
wrappedBody = positiveApiResponse({ data: body });
|
|
29
|
+
}
|
|
30
|
+
send(event, JSON.stringify(wrappedBody), "application/json");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ApiResponse } from '@ipa-schema/api';
|
|
2
|
+
/**
|
|
3
|
+
* 判断是否为 API 路径
|
|
4
|
+
* @param path - 请求路径
|
|
5
|
+
*/
|
|
6
|
+
export declare function isApiPath(path: string | undefined): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* 判断响应体是否已被 defineApiHandler 包装
|
|
9
|
+
* @param body - 响应体
|
|
10
|
+
* @returns 包含 success 字段则视为已包装
|
|
11
|
+
*/
|
|
12
|
+
export declare function isAlreadyWrappedResponse(body: unknown): body is ApiResponse<unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* 判断是否为 HTML 响应(SPA fallback)
|
|
15
|
+
* @param body - 响应体
|
|
16
|
+
*/
|
|
17
|
+
export declare function isHtmlResponse(body: unknown): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function isApiPath(path) {
|
|
2
|
+
return typeof path === "string" && path.startsWith("/api/");
|
|
3
|
+
}
|
|
4
|
+
export function isAlreadyWrappedResponse(body) {
|
|
5
|
+
return body !== null && typeof body === "object" && "success" in body;
|
|
6
|
+
}
|
|
7
|
+
export function isHtmlResponse(body) {
|
|
8
|
+
if (typeof body === "string")
|
|
9
|
+
return body.trimStart().startsWith("<!");
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** 服务端 AuthGuard:校验当前请求是否已通过指定 provider 认证 */
|
|
2
|
+
export interface AuthGuard {
|
|
3
|
+
validate: (event: any) => Promise<boolean>;
|
|
4
|
+
}
|
|
5
|
+
export declare function registerAuthGuard(providerId: string, guard: AuthGuard): void;
|
|
6
|
+
export declare function getAuthGuard(providerId: string): AuthGuard | undefined;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EventHandler, EventHandlerRequest } from 'h3';
|
|
2
|
+
/**
|
|
3
|
+
* use `defineApiHandler` insteadOf `defineEventHandler` to wrap the api handler
|
|
4
|
+
*
|
|
5
|
+
* @param handler the route handler
|
|
6
|
+
* @returns the wrapped route handler
|
|
7
|
+
*/
|
|
8
|
+
export declare function defineApiHandler<T extends EventHandlerRequest, D>(handler: EventHandler<T, D>): EventHandler<T, D>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineEventHandler } from "h3";
|
|
2
|
+
import {
|
|
3
|
+
negativeApiResponse,
|
|
4
|
+
positiveApiResponse,
|
|
5
|
+
toApiError
|
|
6
|
+
} from "@ipa-schema/api";
|
|
7
|
+
export function defineApiHandler(handler) {
|
|
8
|
+
return defineEventHandler(async (event) => {
|
|
9
|
+
try {
|
|
10
|
+
const response = await handler(event);
|
|
11
|
+
return positiveApiResponse({
|
|
12
|
+
data: response
|
|
13
|
+
});
|
|
14
|
+
} catch (err) {
|
|
15
|
+
return negativeApiResponse(toApiError(err));
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface LlmRequestOptions {
|
|
2
|
+
messages: any[];
|
|
3
|
+
system?: string;
|
|
4
|
+
tools?: Record<string, {
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: any;
|
|
7
|
+
}>;
|
|
8
|
+
model?: string;
|
|
9
|
+
baseURL?: string;
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function handleLlmRequest(options: LlmRequestOptions): Promise<Response>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { createError } from "h3";
|
|
3
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
4
|
+
import { convertToModelMessages, jsonSchema, streamText } from "ai";
|
|
5
|
+
export async function handleLlmRequest(options) {
|
|
6
|
+
const {
|
|
7
|
+
messages,
|
|
8
|
+
system,
|
|
9
|
+
tools: rawTools,
|
|
10
|
+
model = process.env.LLM_MODEL || "deepseek-chat",
|
|
11
|
+
baseURL = process.env.LLM_BASE_URL || "https://api.deepseek.com",
|
|
12
|
+
apiKey = process.env.LLM_API_KEY
|
|
13
|
+
} = options;
|
|
14
|
+
if (!apiKey) {
|
|
15
|
+
throw createError({
|
|
16
|
+
statusCode: 500,
|
|
17
|
+
statusMessage: "LLM_API_KEY \u672A\u914D\u7F6E\uFF0C\u8BF7\u5728 .env \u4E2D\u8BBE\u7F6E"
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const modelMessages = await convertToModelMessages(messages);
|
|
21
|
+
const openai = createOpenAI({ baseURL, apiKey });
|
|
22
|
+
const tools = rawTools ? Object.fromEntries(
|
|
23
|
+
Object.entries(rawTools).map(([name, def]) => [
|
|
24
|
+
name,
|
|
25
|
+
{ description: def.description, inputSchema: jsonSchema(def.inputSchema) }
|
|
26
|
+
])
|
|
27
|
+
) : void 0;
|
|
28
|
+
const result = streamText({
|
|
29
|
+
model: openai.chat(model),
|
|
30
|
+
system,
|
|
31
|
+
messages: modelMessages,
|
|
32
|
+
tools
|
|
33
|
+
});
|
|
34
|
+
return result.toUIMessageStreamResponse();
|
|
35
|
+
}
|