Browse Source

feat(app): minimal login register profile pages

Made-with: Cursor
feat/auth-user
npmrun 6 days ago
parent
commit
38ab5a14aa
  1. 8
      app/middleware/auth.ts
  2. 9
      app/middleware/guest.ts
  3. 41
      app/pages/login.vue
  4. 81
      app/pages/me.vue
  5. 56
      app/pages/register.vue

8
app/middleware/auth.ts

@ -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 } });
}
});

9
app/middleware/guest.ts

@ -0,0 +1,9 @@
export default defineNuxtRouteMiddleware(async () => {
const fetch = useRequestFetch();
try {
await fetch("/api/me", { credentials: "include" });
return navigateTo("/me");
} catch {
return;
}
});

41
app/pages/login.vue

@ -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>

81
app/pages/me.vue

@ -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>

56
app/pages/register.vue

@ -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…
Cancel
Save