Browse Source

feat(auth): enhance authentication flow with user context and error handling

auth
npmrun 4 weeks ago
parent
commit
3f12e95180
  1. 12
      app/composables/useAuth.ts
  2. 18
      app/middleware/auth.global.ts
  3. 4
      app/pages/index/index.vue
  4. 4
      app/plugins/auth.ts
  5. BIN
      packages/drizzle-pkg/db.sqlite
  6. 2
      server/api/auth/captcha.post.ts
  7. 2
      server/api/auth/login.post.ts
  8. 2
      server/api/auth/logout.post.ts
  9. 15
      server/api/auth/me.get.ts
  10. 2
      server/api/auth/register.post.ts
  11. 2
      server/api/file/upload.post.ts
  12. 3
      server/api/hello.get.ts
  13. 6
      server/api/pic/random.get.ts
  14. 9
      server/types/index.d.ts
  15. 18
      server/utils/context.ts
  16. 25
      server/utils/handler.ts

12
app/composables/useAuth.ts

@ -24,9 +24,15 @@ export function useAuth() {
const loading = ref(false);
async function loadUser() {
const res = await $fetch<ApiResponse<{ user: User }>>("/api/auth/me");
if (res.code === 0 && res.data?.user) {
user.value = res.data.user;
try {
const res = await $fetch<ApiResponse<{ user: User }>>("/api/auth/me", {
headers: process.server ? useRequestHeaders(["cookie"]) : {},
});
if (res.code === 0 && res.data?.user) {
user.value = res.data.user;
}
} catch {
// 未登录时静默处理
}
}

18
app/middleware/auth.global.ts

@ -1,14 +1,14 @@
const PUBLIC_PATHS = ["/", "/index", "/login", "/register"];
const PUBLIC_PATHS = ["/", "/index"];
const AUTH_PATHS = ["/login", "/register"];
export default defineNuxtRouteMiddleware(async (to) => {
if (PUBLIC_PATHS.includes(to.path)) return;
export default defineNuxtRouteMiddleware((to) => {
const { isLoggedIn } = useAuth();
try {
const res = await $fetch<{ code: number }>("/api/auth/me");
if (res.code !== 0) {
return navigateTo("/login?redirect=" + encodeURIComponent(to.fullPath));
}
} catch {
if (isLoggedIn.value && AUTH_PATHS.includes(to.path)) {
return navigateTo("/");
}
if (!AUTH_PATHS.includes(to.path) && !PUBLIC_PATHS.includes(to.path) && !isLoggedIn.value) {
return navigateTo("/login?redirect=" + encodeURIComponent(to.fullPath));
}
});

4
app/pages/index/index.vue

@ -1,10 +1,12 @@
<script setup lang="ts">
const { user, isLoggedIn } = useAuth()
</script>
<template>
<div class="flex flex-col items-center justify-center min-h-screen">
<h1 class="text-4xl font-bold mb-4">Welcome to Nuxt 3!</h1>
<p class="text-lg text-gray-600">This is the index page.</p>
<p>{{ isLoggedIn }}</p>
<p>{{ user }}</p>
</div>
</template>

4
app/plugins/auth.ts

@ -0,0 +1,4 @@
export default defineNuxtPlugin(async () => {
const { loadUser } = useAuth()
await loadUser()
})

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.

2
server/api/auth/captcha.post.ts

@ -1,6 +1,6 @@
import { createCaptcha } from "#server/service/auth";
export default defineWrappedResponseHandler(async () => {
export default defineWrappedResponseHandler({ auth: 'public' }, async () => {
const result = await createCaptcha();
return R.success(result);
});

2
server/api/auth/login.post.ts

@ -8,7 +8,7 @@ const loginSchema = z.object({
captchaCode: z.string().min(1),
});
export default defineWrappedResponseHandler(async (event) => {
export default defineWrappedResponseHandler({ auth: 'public' }, async (event) => {
const body = await readBody(event);
const parsed = loginSchema.safeParse(body);

2
server/api/auth/logout.post.ts

@ -1,4 +1,4 @@
export default defineWrappedResponseHandler(async (event) => {
export default defineWrappedResponseHandler({ auth: 'public' }, async (event) => {
deleteCookie(event, "token", { path: "/" });
return R.success(null);
});

15
server/api/auth/me.get.ts

@ -1,16 +1,5 @@
import { getUserFromEvent } from "#server/utils/jwt";
import { getCurrentUser } from "#server/service/auth";
import { getContextUser } from "#server/utils/context";
export default defineWrappedResponseHandler(async (event) => {
const payload = getUserFromEvent(event);
if (!payload) {
return R.error("未登录", null);
}
const user = await getCurrentUser(payload);
if (!user) {
return R.error("用户不存在", null);
}
return R.success({ user });
return R.success({ user: getContextUser(event) });
});

2
server/api/auth/register.post.ts

@ -8,7 +8,7 @@ const registerSchema = z.object({
captchaCode: z.string().min(1),
});
export default defineWrappedResponseHandler(async (event) => {
export default defineWrappedResponseHandler({ auth: 'public' }, async (event) => {
const body = await readBody(event);
const parsed = registerSchema.safeParse(body);

2
server/api/file/upload.post.ts

@ -12,7 +12,7 @@ interface IFile {
size: number;
}
export default defineWrappedResponseHandler(async (event) => {
export default defineWrappedResponseHandler({ auth: 'optional' }, async (event) => {
try {
// 存储目录
const uploadDir = path.join(process.cwd(), 'public/assets');

3
server/api/hello.get.ts

@ -1,8 +1,7 @@
import { getUsers } from "../service/auth"
import { compare, hash } from "bcryptjs";
export default defineWrappedResponseHandler(async (event) => {
compare
export default defineWrappedResponseHandler({ auth: 'public' }, async (event) => {
const users = await getUsers()
return R.success({
hello: "aa",

6
server/api/pic/random.get.ts

@ -19,15 +19,11 @@ const handler = eventHandler(async (event: H3Event) => {
return "error"
}
if (Reflect.has(query, "miaomc")) {
// return await $fetch("https://api.miaomc.cn/image/get", { method: "get", mode: "cors" })
event.node.res.statusCode = 302;
event.node.res.setHeader("location", "https://api.miaomc.cn/image/get");
return;
}
if (Reflect.has(query, "r10086")) {
// return `<!DOCTYPE html><html lang="zh"><head><meta charset="utf-8"><title>选择</title></head>
// <body><script>location.href="https://api.r10086.com/樱道随机图片api接口.php?图片系列=动漫综合1"</script></body>
// </html>`;
return await sendRedirect(
event,
encodeURI("https://api.r10086.com/樱道随机图片api接口.php?图片系列=动漫综合1"),
@ -37,7 +33,7 @@ const handler = eventHandler(async (event: H3Event) => {
if (Reflect.has(query, "favicon")) {
const avatarPath = resolve("public", "favicon.ico")
event.node.res.setHeader("Content-Type", "image/jpeg");
return fs.readFile(avatarPath) //fs.createReadStream(avatarPath);
return fs.readFile(avatarPath)
}
return `<!DOCTYPE html><html lang="zh"><head><meta charset="utf-8"><title>选择</title></head>
<body>

9
server/types/index.d.ts

@ -0,0 +1,9 @@
import type { getCurrentUser } from "#server/service/auth";
type CurrentUser = Exclude<Awaited<ReturnType<typeof getCurrentUser>>, null>;
declare module "h3" {
interface H3EventContext {
user?: CurrentUser;
}
}

18
server/utils/context.ts

@ -0,0 +1,18 @@
import type { H3Event } from "h3";
import type { getCurrentUser } from "#server/service/auth";
type CurrentUser = Exclude<Awaited<ReturnType<typeof getCurrentUser>>, null>;
/**
* event.context
*/
export function setContextUser(event: H3Event, user: CurrentUser): void {
(event.context as Record<string, unknown>).user = user;
}
/**
* event.context
*/
export function getContextUser(event: H3Event): CurrentUser | undefined {
return (event.context as Record<string, unknown>).user as CurrentUser | undefined;
}

25
server/utils/handler.ts

@ -1,11 +1,14 @@
import log4js from "logger";
import { getUserFromEvent } from "#server/utils/jwt";
import { getCurrentUser } from "#server/service/auth";
import { setContextUser } from "#server/utils/context";
interface IConfig {
auth?: 'required' | 'public' | 'optional';
}
const defaultConfig: IConfig = {
auth: 'required',
}
const logger = log4js.getLogger("ERROR");
@ -21,6 +24,24 @@ export const defineWrappedResponseHandler = <T extends EventHandlerRequest, D>(
const config = Object.assign({ ...defaultConfig }, typeof handlerOrConfig === 'object' ? handlerOrConfig : {});
return defineEventHandler<T>(async (event) => {
// ---- auth guard ----
if (config.auth !== 'public') {
const payload = getUserFromEvent(event);
if (config.auth === 'required' && !payload) {
return R.error("未登录", null);
}
if (payload) {
const user = await getCurrentUser(payload);
if (config.auth === 'required' && !user) {
return R.error("用户不存在", null);
}
if (user) {
setContextUser(event, user);
}
}
}
// ---- end auth guard ----
const response = await handler(event)
return response
})

Loading…
Cancel
Save