5 changed files with 195 additions and 0 deletions
@ -0,0 +1,8 @@ |
|||||
|
export default defineNuxtRouteMiddleware(async (to) => { |
||||
|
const fetch = useRequestFetch(); |
||||
|
try { |
||||
|
await fetch("/api/me", { credentials: "include" }); |
||||
|
} catch { |
||||
|
return navigateTo({ path: "/login", query: { redirect: to.fullPath } }); |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,9 @@ |
|||||
|
export default defineNuxtRouteMiddleware(async () => { |
||||
|
const fetch = useRequestFetch(); |
||||
|
try { |
||||
|
await fetch("/api/me", { credentials: "include" }); |
||||
|
return navigateTo("/me"); |
||||
|
} catch { |
||||
|
return; |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,41 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
definePageMeta({ middleware: "guest" }); |
||||
|
|
||||
|
const email = ref(""); |
||||
|
const password = ref(""); |
||||
|
const errorMsg = ref(""); |
||||
|
|
||||
|
async function onSubmit() { |
||||
|
errorMsg.value = ""; |
||||
|
try { |
||||
|
await $fetch("/api/auth/login", { |
||||
|
method: "POST", |
||||
|
body: { email: email.value, password: password.value }, |
||||
|
credentials: "include", |
||||
|
}); |
||||
|
await navigateTo("/me"); |
||||
|
} catch (e: unknown) { |
||||
|
const err = e as { data?: { error?: { message?: string } } }; |
||||
|
errorMsg.value = err.data?.error?.message ?? "登录失败"; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div style="max-width: 360px; margin: 2rem auto; font-family: system-ui"> |
||||
|
<h1>登录</h1> |
||||
|
<form @submit.prevent="onSubmit"> |
||||
|
<label> |
||||
|
邮箱 |
||||
|
<input v-model="email" type="email" required style="display: block; width: 100%; margin: 0.25rem 0 1rem" /> |
||||
|
</label> |
||||
|
<label> |
||||
|
密码 |
||||
|
<input v-model="password" type="password" minlength="8" required style="display: block; width: 100%; margin: 0.25rem 0 1rem" /> |
||||
|
</label> |
||||
|
<p v-if="errorMsg" style="color: crimson">{{ errorMsg }}</p> |
||||
|
<button type="submit">登录</button> |
||||
|
</form> |
||||
|
<p><NuxtLink to="/register">没有账号?注册</NuxtLink></p> |
||||
|
</div> |
||||
|
</template> |
||||
@ -0,0 +1,81 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
definePageMeta({ middleware: "auth" }); |
||||
|
|
||||
|
type User = { |
||||
|
id: number; |
||||
|
name: string; |
||||
|
age: number; |
||||
|
email: string; |
||||
|
emailVerified: boolean; |
||||
|
}; |
||||
|
|
||||
|
const fetch = useRequestFetch(); |
||||
|
const { data, refresh } = await useAsyncData("me", () => |
||||
|
fetch<{ user: User }>("/api/me", { credentials: "include" }), |
||||
|
); |
||||
|
|
||||
|
const name = ref(data.value?.user.name ?? ""); |
||||
|
const age = ref(data.value?.user.age ?? 0); |
||||
|
const patchError = ref(""); |
||||
|
|
||||
|
watch( |
||||
|
() => data.value?.user, |
||||
|
(u) => { |
||||
|
if (u) { |
||||
|
name.value = u.name; |
||||
|
age.value = u.age; |
||||
|
} |
||||
|
}, |
||||
|
{ immediate: true }, |
||||
|
); |
||||
|
|
||||
|
async function saveProfile() { |
||||
|
patchError.value = ""; |
||||
|
try { |
||||
|
await $fetch("/api/me", { |
||||
|
method: "PATCH", |
||||
|
body: { name: name.value, age: age.value }, |
||||
|
credentials: "include", |
||||
|
}); |
||||
|
await refresh(); |
||||
|
} catch (e: unknown) { |
||||
|
const err = e as { statusCode?: number; data?: { error?: { message?: string } } }; |
||||
|
if (err.statusCode === 403) { |
||||
|
patchError.value = "需先完成邮箱验证后才能修改资料。"; |
||||
|
} else { |
||||
|
patchError.value = err.data?.error?.message ?? "保存失败"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function logout() { |
||||
|
await $fetch("/api/auth/logout", { method: "POST", credentials: "include" }); |
||||
|
await navigateTo("/login"); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div style="max-width: 420px; margin: 2rem auto; font-family: system-ui"> |
||||
|
<h1>个人资料</h1> |
||||
|
<p v-if="data?.user"> |
||||
|
邮箱:{{ data.user.email }} |
||||
|
<span v-if="data.user.emailVerified">(已验证)</span> |
||||
|
<span v-else>(未验证)</span> |
||||
|
</p> |
||||
|
<p v-if="patchError" style="color: darkorange">{{ patchError }}</p> |
||||
|
<form @submit.prevent="saveProfile"> |
||||
|
<label> |
||||
|
姓名 |
||||
|
<input v-model="name" type="text" required style="display: block; width: 100%; margin: 0.25rem 0 1rem" /> |
||||
|
</label> |
||||
|
<label> |
||||
|
年龄 |
||||
|
<input v-model.number="age" type="number" min="1" max="150" required style="display: block; width: 100%; margin: 0.25rem 0 1rem" /> |
||||
|
</label> |
||||
|
<button type="submit">保存</button> |
||||
|
</form> |
||||
|
<p style="margin-top: 2rem"> |
||||
|
<button type="button" @click="logout">退出登录</button> |
||||
|
</p> |
||||
|
</div> |
||||
|
</template> |
||||
@ -0,0 +1,56 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
definePageMeta({ middleware: "guest" }); |
||||
|
|
||||
|
const email = ref(""); |
||||
|
const password = ref(""); |
||||
|
const name = ref(""); |
||||
|
const age = ref(20); |
||||
|
const errorMsg = ref(""); |
||||
|
|
||||
|
async function onSubmit() { |
||||
|
errorMsg.value = ""; |
||||
|
try { |
||||
|
await $fetch("/api/auth/register", { |
||||
|
method: "POST", |
||||
|
body: { |
||||
|
email: email.value, |
||||
|
password: password.value, |
||||
|
name: name.value, |
||||
|
age: Number(age.value), |
||||
|
}, |
||||
|
credentials: "include", |
||||
|
}); |
||||
|
await navigateTo("/me"); |
||||
|
} catch (e: unknown) { |
||||
|
const err = e as { data?: { error?: { message?: string } } }; |
||||
|
errorMsg.value = err.data?.error?.message ?? "注册失败"; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div style="max-width: 360px; margin: 2rem auto; font-family: system-ui"> |
||||
|
<h1>注册</h1> |
||||
|
<form @submit.prevent="onSubmit"> |
||||
|
<label> |
||||
|
邮箱 |
||||
|
<input v-model="email" type="email" required style="display: block; width: 100%; margin: 0.25rem 0 1rem" /> |
||||
|
</label> |
||||
|
<label> |
||||
|
密码(至少 8 位) |
||||
|
<input v-model="password" type="password" minlength="8" required style="display: block; width: 100%; margin: 0.25rem 0 1rem" /> |
||||
|
</label> |
||||
|
<label> |
||||
|
姓名 |
||||
|
<input v-model="name" type="text" required style="display: block; width: 100%; margin: 0.25rem 0 1rem" /> |
||||
|
</label> |
||||
|
<label> |
||||
|
年龄 |
||||
|
<input v-model.number="age" type="number" min="1" max="150" required style="display: block; width: 100%; margin: 0.25rem 0 1rem" /> |
||||
|
</label> |
||||
|
<p v-if="errorMsg" style="color: crimson">{{ errorMsg }}</p> |
||||
|
<button type="submit">注册并登录</button> |
||||
|
</form> |
||||
|
<p><NuxtLink to="/login">已有账号?登录</NuxtLink></p> |
||||
|
</div> |
||||
|
</template> |
||||
Loading…
Reference in new issue