feat(/api/user): Compete login-code and auth-middleware

This commit is contained in:
漩葵 2024-06-22 12:19:52 +08:00
parent 1abe25849b
commit ba813ffcc8
25 changed files with 396 additions and 183 deletions

View File

@ -24,11 +24,12 @@
<el-menu-item index="/area/jx2/game">游戏服务器</el-menu-item> <el-menu-item index="/area/jx2/game">游戏服务器</el-menu-item>
</el-sub-menu> --> </el-sub-menu> -->
</el-sub-menu> </el-sub-menu>
<el-menu-item index="/forum" disabled>论坛</el-menu-item> <el-menu-item index="forum" disabled>论坛</el-menu-item>
<el-menu-item index="/host" disabled>托管</el-menu-item> <el-menu-item index="host" disabled>托管</el-menu-item>
<div class="flex-grow" /> <div class="flex-grow" />
<el-menu-item index="/login">登录</el-menu-item> <el-menu-item v-if="auth" index="/user/login">登录</el-menu-item>
<el-menu-item index="register">注册</el-menu-item> <el-menu-item v-if="auth" index="/user/register">注册</el-menu-item>
<el-menu-item v-if="!auth" @click="logoutNow">注销</el-menu-item>
</el-menu> </el-menu>
</client-only> </client-only>
</template> </template>
@ -36,9 +37,13 @@
<script lang="ts" setup> <script lang="ts" setup>
const activeIndex = ref("1"); const activeIndex = ref("1");
const handleSelect = (key: string, keyPath: string[]) => { const handleSelect = (key: string, keyPath: string[]) => {
console.log(key, keyPath);
navigateTo(key); navigateTo(key);
}; };
const auth = ref(useCookie("auth").value == undefined);
function logoutNow() {
logout();
auth.value = true;
}
</script> </script>
<style> <style>
.flex-grow { .flex-grow {

14
composables/logout.ts Normal file
View File

@ -0,0 +1,14 @@
// 它将作为 useFoo() 可用(文件名的驼峰形式,不包括扩展名)
export default function () {
const auth = useCookie("auth");
$fetch("/api/user/logout", {
method: "POST",
body: {
auth: auth.value,
},
});
auth.value = undefined;
navigateTo("/");
return 1;
}

0
middleware/admin.ts Normal file
View File

27
middleware/auth.ts Normal file
View File

@ -0,0 +1,27 @@
export default defineNuxtRouteMiddleware(async (to, from) => {
const auth = useCookie("auth");
if (auth.value === undefined) {
ElMessage("未登录或cookie未开启");
return navigateTo("/user/login", { replace: true });
} else {
const { data: result } = await useFetch("/api/user/auth", {
method: "post",
body: {
auth: auth.value,
},
});
if (!result.value?.login && to.path !== "/user/test") {
if (result.value?.code == 0) {
ElMessage("未登录");
} else if (result.value?.code == 2) {
ElMessage("登录超时,请重新登录");
auth.value = undefined;
} else {
ElMessage(result.value?.code);
}
return navigateTo("/user/login");
} else {
console.log(auth.value);
}
}
});

7
middleware/unauth.ts Normal file
View File

@ -0,0 +1,7 @@
export default defineNuxtRouteMiddleware(async (to, from) => {
const auth = useCookie("auth");
if (auth.value === undefined) {
} else {
return navigateTo("/");
}
});

View File

@ -0,0 +1,23 @@
<template>
<Head>
<Title>登录日志</Title>
<Meta name="description" />
</Head>
<client-only>
<el-table
:default-sort="{ prop: 'id', order: 'descending' }"
:data="tableData"
style="width: 100%"
>
<el-table-column prop="id" label="id" width="50" />
<el-table-column prop="username" label="用户" width="180" />
<el-table-column prop="date" label="登录时间" width="180" />
<el-table-column prop="ip" label="登录ip" width="180" />
</el-table>
</client-only>
</template>
<script lang="ts" setup>
import type { LoginLog } from "~/types/Log";
const data: LoginLog[] = await $fetch("/api/admin/loginlogs");
const tableData = ref(data);
</script>

View File

@ -17,42 +17,6 @@
view="tabs" view="tabs"
style="padding: 30px" style="padding: 30px"
> >
<StaticElement
tag="h4"
align="center"
content="个人信息"
name="static"
/>
<TextElement
name="username"
label="昵称"
placeholder="昵称"
:columns="{ container: 4, label: 3, wrapper: 12 }"
rules="required|min:3"
/>
<PhoneElement
name="phone"
allow-incomplete
unmask
default="+86"
:include="['cn']"
label="手机号"
placeholder="+86"
:columns="{ container: 12, label: 1, wrapper: 4 }"
rules="required"
/>
<TextElement
name="code"
label="验证码"
placeholder="xxxxxx"
:columns="{ container: 4, label: 4, wrapper: 12 }"
rules="required"
/>
<ButtonElement
name="button"
:columns="{ container: 8, label: 3, wrapper: 12 }"
button-label="发送验证码"
/>
<StaticElement <StaticElement
tag="h4" tag="h4"
align="center" align="center"
@ -119,5 +83,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import PhoneElemen from "@vueform/vueform"; import PhoneElemen from "@vueform/vueform";
definePageMeta({
middleware: ["auth"],
});
const form = ref(); const form = ref();
</script> </script>

View File

@ -4,6 +4,7 @@
<Title>FreePotato Server</Title> <Title>FreePotato Server</Title>
<Meta name="description" content="免费服务器~" /> <Meta name="description" content="免费服务器~" />
</Head> </Head>
<ElRow :gutter="10" align="middle" style="height: 900px"> <ElRow :gutter="10" align="middle" style="height: 900px">
<ElCol :span="24"><IndexNewsCarouel /></ElCol> <ElCol :span="24"><IndexNewsCarouel /></ElCol>
<ElCol :span="24"><IndexNewsStatus /></ElCol> <ElCol :span="24"><IndexNewsStatus /></ElCol>

View File

@ -1,63 +0,0 @@
<template>
<!--登录界面-->
<Head>
<Title>登录界面</Title>
<Meta name="description" />
</Head>
<div
style="width: 25%; margin: auto"
:style="{
boxShadow: `var(--el-box-shadow-light)`,
}"
>
<ClientOnly>
<Vueform
endpoint="/api/test"
method="POST"
view="tabs"
style="padding: 30px"
>
<StaticElement tag="h4" align="center" content="登录" name="static" />
<PhoneElement
input-type="number"
name="phone"
allow-incomplete
unmask
default="+86"
:include="['cn']"
label="手机号"
placeholder="+86"
:columns="{ container: 12, label: 3, wrapper: 10 }"
rules="required|min:14|max:14"
/>
<TextElement
input-type="password"
name="password"
label="密码"
rules="required|min:8|max:16|confirmed"
:columns="{ container: 12, label: 3, wrapper: 10 }"
/>
<CheckboxElement
name="policy"
label="用户协议"
rules="required"
message="您必须同意用户政策才能继续"
>
已阅读并同意<ElLink href="/" target="_blank">用户协议</ElLink>
</CheckboxElement>
<CheckboxElement name="news"> 希望获取最新资讯 </CheckboxElement>
<ButtonElement
name="submit"
button-label="登录"
align="center"
size="lg"
submits
/>
</Vueform>
</ClientOnly>
</div>
</template>
<script setup lang="ts">
import PhoneElemen from "@vueform/vueform";
const form = ref();
</script>

View File

@ -4,3 +4,8 @@
<Meta name="description" /> <Meta name="description" />
</Head> </Head>
</template> </template>
<script setup lang="ts">
definePageMeta({
middleware: ["auth"],
});
</script>

111
pages/user/login/index.vue Normal file
View File

@ -0,0 +1,111 @@
<template>
<!--登录界面-->
<Head>
<Title>登录界面</Title>
<Meta name="description" />
</Head>
<div
class="formbox"
:style="{
boxShadow: `var(--el-box-shadow-light)`,
}"
>
<ClientOnly>
<Vueform
endpoint="/api/user/login"
method="POST"
view="tabs"
style="padding: 30px"
@success="PostSuccess"
>
<StaticElement tag="h4" align="center" content="登录" name="static" />
<TextElement
ref="phone$"
name="phone"
allow-incomplete
label="手机号"
placeholder="+86"
:columns="{ container: 12, label: 3, wrapper: 10 }"
rules="required|min:11|max:11"
/>
<TextElement
input-type="password"
name="password"
allow-incomplete
label="密码"
rules="required|min:8|max:16"
:columns="{ container: 12, label: 3, wrapper: 10 }"
/>
<HiddenElement :default="ip" name="client_ip" />
<CheckboxElement
name="policy"
label="用户协议"
rules="required"
message="您必须同意用户政策才能继续"
>
已阅读并同意<ElLink href="/" target="_blank">用户协议</ElLink>
</CheckboxElement>
<ButtonElement
name="submit"
button-label="登录"
align="center"
size="lg"
submits
/>
</Vueform>
</ClientOnly>
</div>
</template>
<script setup lang="ts">
import PhoneElemen from "@vueform/vueform";
definePageMeta({
middleware: ["unauth"],
});
const form = ref();
const data: { query: string } = await $fetch("http://ip-api.com/json");
const ip = data.query;
function PostSuccess(response: {
data: {
code: number;
msg: string;
token: string;
};
}) {
if (response.data.code == 1) {
ElMessage({ message: response.data.msg, type: "success" });
const auth = useCookie("auth", { maxAge: 60 * 30 });
auth.value = response.data.token;
location.reload();
navigateTo("/", { replace: true });
} else if (response.data.code == -1) {
ElMessage({ message: response.data.msg, type: "warning" });
navigateTo("/user/register");
} else {
ElMessage({ message: response.data.msg, type: "warning" });
}
}
</script>
<style>
.formbox {
width: 25%;
margin: auto;
}
@media screen and (min-width: 901px) and (max-width: 1200px) {
.formbox {
width: 50%;
margin: auto;
}
}
@media screen and (min-width: 601px) and (max-width: 900px) {
.formbox {
width: 75%;
margin: auto;
}
}
@media screen and (max-width: 600px) {
.formbox {
width: 88%;
margin: auto;
}
}
</style>

9
pages/user/logout.ts Normal file
View File

@ -0,0 +1,9 @@
const auth = useCookie("auth");
$fetch("/api/user/logout", {
method: "POST",
body: {
auth: auth.value,
},
});
auth.value = undefined;
navigateTo("/");

View File

@ -17,6 +17,7 @@
view="tabs" view="tabs"
:model-value="form" :model-value="form"
@update:model-value="form = $event" @update:model-value="form = $event"
@success="RegSuccess"
validate-on="change" validate-on="change"
style="padding: 5%" style="padding: 5%"
> >
@ -31,18 +32,14 @@
rules="required|min:3|max:64" rules="required|min:3|max:64"
@input="Check()" @input="Check()"
/> />
<PhoneElement <TextElement
ref="phone$" ref="phone$"
input-type="number"
name="phone" name="phone"
allow-incomplete allow-incomplete
unmask
default="+86"
:include="['cn']"
label="手机号" label="手机号"
placeholder="+86" placeholder="+86"
:columns="{ container: 12, label: 3, wrapper: 10 }" :columns="{ container: 12, label: 3, wrapper: 10 }"
rules="required|min:14|max:14" rules="required|min:11|max:11"
@input="Check()" @input="Check()"
:disabled="phoneChecked" :disabled="phoneChecked"
/> />
@ -95,7 +92,6 @@
> >
已阅读并同意<ElLink href="/" target="_blank">用户协议</ElLink> 已阅读并同意<ElLink href="/" target="_blank">用户协议</ElLink>
</CheckboxElement> </CheckboxElement>
<CheckboxElement name="news"> 希望获取最新资讯 </CheckboxElement>
<ButtonElement <ButtonElement
:disabled="canSubmit" :disabled="canSubmit"
name="submit" name="submit"
@ -134,30 +130,33 @@ function sliderHandleError() {
function Check() { function Check() {
const query = { const query = {
username: form.value.username, username: form.value.username,
phone: form.value.phone.replace("+86", ""), phone: form.value.phone || "",
}; };
if (!/^(?!1(4|7)\d{9})1[3-9]\d{9}$/.test(query.phone)) { if (!/^(?!1(4|7)\d{9})1[3-9]\d{9}$/.test(query.phone) && query.phone != "") {
phone$.value.messageBag.clear("errors"); username$.value.messageBag.clear("errors");
phone$.value.messageBag.append("手机号格式错误" + query.phone); username$.value.messageBag.append("手机号格式错误" + query.phone);
canSubmit.value = true; canSubmit.value = true;
return; return;
} else { } else {
phone$.value.messageBag = []; username$.value.messageBag.clear("errors");
} }
$fetch("/api/test/reg", { $fetch("/api/reg", {
method: "POST", method: "POST",
body: query, body: query,
}).then((res) => { }).then((res) => {
if (!res.phone) { if (!res.phone) {
phone$.value.messageBag.append("手机号已经注册"); username$.value.messageBag.append("手机号已经注册");
canSubmit.value = true; canSubmit.value = true;
return 0;
} else { } else {
username$.value.messageBag.clear("errors");
canSubmit.value = false; canSubmit.value = false;
} }
if (!res.username) { if (!res.username) {
username$.value.messageBag.append("昵称已存在"); username$.value.messageBag.append("昵称已存在");
canSubmit.value = true; canSubmit.value = true;
} else { } else {
username$.value.messageBag.clear("errors");
canSubmit.value = false; canSubmit.value = false;
} }
}); });
@ -170,10 +169,10 @@ function Send() {
Check(); Check();
if (!canSubmit.value) { if (!canSubmit.value) {
$fetch("/api/test/sms", { $fetch("/api/reg/sms", {
method: "POST", method: "POST",
body: { body: {
phone: form.value.phone.replace("+86", ""), phone: form.value.phone,
}, },
}).then((res) => { }).then((res) => {
switch (res.code) { switch (res.code) {
@ -199,7 +198,7 @@ function Send() {
} }
function CheckCode() { function CheckCode() {
if (!code$.value.invalid) { if (!code$.value.invalid) {
$fetch("/api/test/code", { $fetch("/api/reg/code", {
method: "POST", method: "POST",
body: { body: {
phone: form.value.phone.replace("+86", ""), phone: form.value.phone.replace("+86", ""),
@ -217,7 +216,7 @@ function CheckCode() {
ElMessage({ message: res.msg, type: "success" }); ElMessage({ message: res.msg, type: "success" });
codeBtnRef.value.disabled = true; codeBtnRef.value.disabled = true;
codeBtnRef.value.text = "已验证"; codeBtnRef.value.text = "已验证";
phoneChecked.value = ture; phoneChecked.value = true;
break; break;
case 2: case 2:
ElMessage({ message: res.msg, type: "warning" }); ElMessage({ message: res.msg, type: "warning" });
@ -231,6 +230,14 @@ function CheckCode() {
}); });
} }
} }
function RegSuccess(response: { data: { code: number; msg: string } }, form$) {
if (response.data.code == 1) {
ElMessage({ message: response.data.msg, type: "success" });
navigateTo("/user/login");
} else {
ElMessage({ message: response.data.msg, type: "warning" });
}
}
</script> </script>
<style> <style>
.formbox { .formbox {

View File

@ -3,6 +3,7 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-3.0.x"]
} }
datasource db { datasource db {
@ -44,9 +45,9 @@ model Register {
} }
model Loginlogs { model Loginlogs {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
phone String outtime DateTime
time DateTime
ip String ip String
loiner User @relation(fields: [userid], references: [id]) loginer User @relation(fields: [userid], references: [id])
userid Int userid Int
token String @unique
} }

View File

@ -0,0 +1,38 @@
import { PrismaClient } from "@prisma/client";
import type { LoginLog } from "~/types/Log";
const db = new PrismaClient();
type log = { loginer: { username: string | null } } & {
id: number;
outtime: Date;
ip: string;
userid: number;
token: string;
};
function formatLogs(raw: log[]): LoginLog[] {
var logs: LoginLog[] = [];
raw.forEach((element: log) => {
logs.push({
id: element.id,
username: element.loginer.username || "none",
date: new Date(element.outtime.getTime() - 1800000).toLocaleTimeString(),
ip: element.ip,
});
});
return logs;
}
export default defineEventHandler(async (event) => {
const loginlogs = await db.loginlogs.findMany({
orderBy: {
outtime: "desc", // 'asc' 表示升序,'desc' 表示降序
},
include: {
loginer: {
select: {
username: true,
},
},
},
});
await db.$disconnect();
return formatLogs(loginlogs);
});

View File

@ -14,6 +14,7 @@ export default defineEventHandler(async (event) => {
code: "", code: "",
deadline: new Date(), deadline: new Date(),
}; };
await db.$disconnect();
const deadlineDate = new Date(register.deadline); const deadlineDate = new Date(register.deadline);
const now = new Date(); const now = new Date();
if (now > deadlineDate) { if (now > deadlineDate) {

View File

@ -2,7 +2,7 @@ import { PrismaClient } from "@prisma/client";
const db = new PrismaClient(); const db = new PrismaClient();
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const body = await readBody(event); const body = await readBody(event);
return { const res = {
username: username:
(await db.user.findFirst({ (await db.user.findFirst({
where: { where: {
@ -16,4 +16,6 @@ export default defineEventHandler(async (event) => {
}, },
})) == null, })) == null,
}; };
await db.$disconnect();
return res;
}); });

View File

@ -83,11 +83,14 @@ export default defineEventHandler(async (event) => {
const deadlineDate = new Date(register.deadline); const deadlineDate = new Date(register.deadline);
const now = new Date(); const now = new Date();
if (now <= deadlineDate) { if (now <= deadlineDate) {
await db.$disconnect();
return { code: 2, msg: "三分钟内请勿重发送" }; return { code: 2, msg: "三分钟内请勿重发送" };
} else { } else {
await db.$disconnect();
return send(body.phone); return send(body.phone);
} }
} else { } else {
await db.$disconnect();
return send(body.phone); return send(body.phone);
} }
}); });

View File

@ -2,37 +2,6 @@ import { PrismaClient } from "@prisma/client";
const db = new PrismaClient(); const db = new PrismaClient();
export default defineEventHandler(async (event) => { export default defineEventHandler((event) => {
const body = await readBody(event); return event;
console.info(body);
let isSend =
(await db.register.findFirst({
where: {
phone: body.phone,
},
})) != null;
if (isSend) {
const register = (await db.register.findFirst({
where: {
phone: body.phone,
},
orderBy: {
deadline: "desc", // 'asc' 表示升序,'desc' 表示降序
},
})) || {
deadline: new Date(),
};
const deadlineDate = new Date(register.deadline);
const now = new Date();
if (now <= deadlineDate) {
return { code: 2, msg: "三分钟内请勿重发送" };
} else {
return {
code: -1,
msg:
"发送成功" + now + "<=" + deadlineDate + ":" + (now <= deadlineDate),
};
}
}
return isSend;
}); });

35
server/api/user/auth.ts Normal file
View File

@ -0,0 +1,35 @@
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
async function auth(auth: string) {
const res = await db.loginlogs.findFirst({
where: { token: auth.toString() },
});
//return JSON.stringify((await res).values)
if (res == null) {
return {
login: false,
code: 0, //未登录
};
} else {
const deadlineDate = new Date(res.outtime);
const now = new Date();
if (now < deadlineDate) {
return {
login: true,
code: 1, //登录状态正常
};
} else {
return {
login: false,
code: 2, //登录超时
};
}
}
}
export default defineEventHandler(async (event) => {
const body = await readBody(event);
return auth(body.auth);
});

View File

@ -2,22 +2,20 @@ import { PrismaClient } from "@prisma/client";
const db = new PrismaClient(); const db = new PrismaClient();
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const body = await readBody(event); const body = await readBody(event);
const register = (await db.register.findFirst({ const register = await db.register.findFirst({
where: { where: {
phone: body.phone, phone: body.phone,
}, },
orderBy: { orderBy: {
deadline: "desc", // 'asc' 表示升序,'desc' 表示降序 deadline: "desc", // 'asc' 表示升序,'desc' 表示降序
}, },
})) || { });
code: "", if (register != null) {
deadline: new Date(),
};
const deadlineDate = new Date(register.deadline.getTime() + 7 * 60 * 1000); const deadlineDate = new Date(register.deadline.getTime() + 7 * 60 * 1000);
const now = new Date(); const now = new Date();
if (now <= deadlineDate) { if (now <= deadlineDate) {
if (register.code != body.code) { if (register.code != body.code) {
return { code: 0, msg: "验证码错误请重新发送" }; return { code: 0, msg: "验证码错误请重新发送" + register };
} else { } else {
await db.user.create({ await db.user.create({
data: { data: {
@ -29,9 +27,18 @@ export default defineEventHandler(async (event) => {
.digest("hex"), .digest("hex"),
}, },
}); });
await db.$disconnect();
return { code: 1, msg: "注册成功" }; return { code: 1, msg: "注册成功" };
} }
} else { } else {
await db.$disconnect();
return {
code: -1,
msg: "验证码超时",
};
}
} else {
await db.$disconnect();
return { return {
code: -1, code: -1,
msg: "验证码超时", msg: "验证码超时",

View File

@ -1,5 +1,10 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
const db = new PrismaClient(); const db = new PrismaClient();
function generateDeadline() {
const now = new Date(); // 获取当前时间
const threeMinutesLater = new Date(now.getTime() + 30 * 60 * 1000); // 加上30分钟
return threeMinutesLater.toISOString(); // 转换为ISO格式字符串
}
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const body = await readBody(event); const body = await readBody(event);
const loginer = await db.user.findFirst({ const loginer = await db.user.findFirst({
@ -17,9 +22,26 @@ export default defineEventHandler(async (event) => {
.update(body.password) .update(body.password)
.digest("hex") .digest("hex")
) { ) {
return { code: 1, msg: "登录成功" }; const token = (await import("crypto"))
.createHash("md5")
.update(body.password + new Date().toISOString())
.digest("hex");
await db.loginlogs.create({
data: {
userid: loginer.id,
outtime: generateDeadline(),
token: token,
ip: body.client_ip,
},
});
await db.$disconnect();
return { code: 1, msg: "登录成功", token: token };
} else { } else {
return { code: 0, msg: "用户名、手机号或密码错误" }; await db.$disconnect();
return {
code: 0,
msg: "用户名、手机号或密码错误" /* + JSON.stringify(body) */,
};
} }
} }
}); });

15
server/api/user/logout.ts Normal file
View File

@ -0,0 +1,15 @@
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
export default defineEventHandler(async (event) => {
const body = await readBody(event);
await db.loginlogs.update({
where: {
token: body.auth,
},
data: {
outtime: new Date().toISOString(),
},
});
await db.$disconnect();
return 1;
});

6
types/Log/LoginLog.ts Normal file
View File

@ -0,0 +1,6 @@
export type LoginLog = {
id: number;
username: string;
date: string;
ip: string;
};

1
types/Log/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export { LoginLog } from "./LoginLog.ts";