feat: add auth flow and login guard for api/web
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<el-container class="layout">
|
||||
<router-view v-if="$route.meta?.fullPage" />
|
||||
<el-container v-else class="layout">
|
||||
<el-aside width="220px" class="sidebar">
|
||||
<div class="logo">Memora</div>
|
||||
<el-menu
|
||||
|
||||
@@ -4,8 +4,10 @@ import Review from '../views/Review.vue'
|
||||
import Statistics from '../views/Statistics.vue'
|
||||
import Words from '../views/Words.vue'
|
||||
import Settings from '../views/Settings.vue'
|
||||
import Login from '../views/Login.vue'
|
||||
|
||||
export const routes = [
|
||||
{ path: '/login', name: 'login', component: Login, meta: { public: true, fullPage: true } },
|
||||
{ path: '/', name: 'dashboard', component: Dashboard },
|
||||
{ path: '/memory', name: 'memory', component: Memory },
|
||||
{ path: '/review', name: 'review', component: Review },
|
||||
|
||||
6
memora-web/src/modules/auth/auth.ts
Normal file
6
memora-web/src/modules/auth/auth.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
const TOKEN_KEY = 'memora_token'
|
||||
|
||||
export const getToken = () => localStorage.getItem(TOKEN_KEY)
|
||||
export const setToken = (token: string) => localStorage.setItem(TOKEN_KEY, token)
|
||||
export const clearToken = () => localStorage.removeItem(TOKEN_KEY)
|
||||
export const isAuthed = () => !!getToken()
|
||||
@@ -1,9 +1,20 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { routes } from '../app/routes'
|
||||
import { isAuthed } from '../modules/auth/auth'
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
router.beforeEach((to) => {
|
||||
const isPublic = !!to.meta?.public
|
||||
if (!isPublic && !isAuthed()) {
|
||||
return '/login'
|
||||
}
|
||||
if (to.path === '/login' && isAuthed()) {
|
||||
return '/'
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
import axios from 'axios'
|
||||
import { clearToken, getToken } from '../modules/auth/auth'
|
||||
|
||||
export const http = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 15000
|
||||
})
|
||||
|
||||
http.interceptors.request.use((config) => {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
config.headers = config.headers || {}
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
http.interceptors.response.use(
|
||||
(res) => res,
|
||||
(err) => {
|
||||
// 统一错误抛出
|
||||
if (err?.response?.status === 401) {
|
||||
clearToken()
|
||||
if (location.pathname !== '/login') {
|
||||
location.href = '/login'
|
||||
}
|
||||
}
|
||||
return Promise.reject(err)
|
||||
}
|
||||
)
|
||||
|
||||
70
memora-web/src/views/Login.vue
Normal file
70
memora-web/src/views/Login.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<el-card class="login-card" shadow="hover">
|
||||
<template #header><strong>登录 Memora</strong></template>
|
||||
<el-form :model="form" @submit.prevent="onSubmit">
|
||||
<el-form-item>
|
||||
<el-input v-model="form.email" placeholder="邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="form.password" type="password" show-password placeholder="密码" />
|
||||
</el-form-item>
|
||||
<el-button type="primary" :loading="loading" style="width:100%" @click="onSubmit">登录</el-button>
|
||||
</el-form>
|
||||
<div class="tips">没有账号会自动注册一个新账号</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { http } from '../services/http'
|
||||
import { setToken } from '../modules/auth/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const form = reactive({ email: '', password: '' })
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!form.email || !form.password) {
|
||||
ElMessage.warning('请填写邮箱和密码')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const loginRes = await http.post('/auth/login', form)
|
||||
const token = loginRes.data?.data?.token
|
||||
if (token) {
|
||||
setToken(token)
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// ignore and try register
|
||||
}
|
||||
|
||||
try {
|
||||
await http.post('/auth/register', { ...form, name: form.email.split('@')[0] || 'memora' })
|
||||
const loginRes = await http.post('/auth/login', form)
|
||||
const token = loginRes.data?.data?.token
|
||||
if (token) {
|
||||
setToken(token)
|
||||
ElMessage.success('注册并登录成功')
|
||||
router.push('/')
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.error || '登录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #f4f5fb; }
|
||||
.login-card { width: 360px; }
|
||||
.tips { margin-top: 12px; font-size: 12px; color: #999; text-align: center; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user