mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-06 03:20:13 +08:00
feat: add login in frontend
* extract icon library from main.js * extract router from main.js * failure with error in passport * add example to swagger
This commit is contained in:
1 parent
b55b7aa1ec
commit
49417c405a
11 files changed
+206
-61
No files matched your search
@@ -1,11 +1,36 @@
|
||||
<script setup></script>
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { login } from '../utils/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const errorMessage = ref('')
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const success = await login(username.value, password.value)
|
||||
if (success) router.push({ name: 'game' })
|
||||
else errorMessage.value = 'Invalid username or password.'
|
||||
} catch (err) {
|
||||
// show error message
|
||||
console.error(err)
|
||||
errorMessage.value = 'Something went wrong.'
|
||||
}
|
||||
}
|
||||
|
||||
const clearError = () => {
|
||||
errorMessage.value = ''
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<!--login page-->
|
||||
<div
|
||||
class="bg-background border-8 border-background-primary flex flex-1 flex-col items-center justify-center rounded-3xl p-10"
|
||||
>
|
||||
<h1 class="text-text-primary text-3xl font-black mb-8">Linux Odyssey</h1>
|
||||
<form action="" class="w-full">
|
||||
<form @submit.prevent="handleSubmit" class="w-full">
|
||||
<div class="mb-6">
|
||||
<label for="email" class="text-sm font-semibold text-text">Email</label
|
||||
><input
|
||||
@@ -13,6 +38,8 @@
|
||||
id="email"
|
||||
class="my-4 bg-background-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-background-secondary"
|
||||
placeholder="Enter your email address"
|
||||
v-model="username"
|
||||
@input="clearError"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
@@ -23,16 +50,19 @@
|
||||
id="password"
|
||||
class="my-4 bg-background-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-background-secondary"
|
||||
placeholder="Enter your password"
|
||||
v-model="password"
|
||||
@input="clearError"
|
||||
/>
|
||||
</div>
|
||||
<router-link
|
||||
type="submit"
|
||||
<p class="text-red-500 flex justify-center" v-if="errorMessage">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
<button
|
||||
class="inline-flex justify-center rounded-lg text-sm font-black py-2 mt-3 bg-text-primary text-background w-full"
|
||||
to="/game"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<span>Log in / Sign up</span>
|
||||
</router-link>
|
||||
<input type="hidden" name="remember" value="true" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,4 +1,14 @@
|
||||
<script setup></script>
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { isAuthenticated, logout } from '../utils/auth'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const handleLogout = () => {
|
||||
logout()
|
||||
router.push({ name: 'auth' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex items-center justify-center mx-3">
|
||||
@@ -46,6 +56,12 @@
|
||||
class="text-text-primary h-5 w-5"
|
||||
/>
|
||||
</button>
|
||||
<button v-if="isAuthenticated" @click="handleLogout">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'arrow-right-from-bracket']"
|
||||
class="text-text-primary h-5 w-5"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
/* import the fontawesome core */
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
/* import specific icons */
|
||||
import {
|
||||
faBars,
|
||||
faArrowLeft,
|
||||
faList,
|
||||
faGear,
|
||||
faTriangleExclamation,
|
||||
faExpand,
|
||||
faTerminal,
|
||||
faArrowRotateLeft,
|
||||
faArrowRightFromBracket,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import {
|
||||
faLightbulb,
|
||||
faCircleQuestion,
|
||||
faCircleRight,
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
/* add icons to the library */
|
||||
library.add(
|
||||
faBars,
|
||||
faArrowLeft,
|
||||
faTriangleExclamation,
|
||||
faGear,
|
||||
faExpand,
|
||||
faList,
|
||||
faLightbulb,
|
||||
faCircleQuestion,
|
||||
faArrowRotateLeft,
|
||||
faCircleRight,
|
||||
faTerminal,
|
||||
faArrowRightFromBracket
|
||||
)
|
||||
+2
-50
@@ -1,59 +1,11 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import './style.css'
|
||||
|
||||
/* import the fontawesome core */
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
/* import font awesome icon component */
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
/* import specific icons */
|
||||
import {
|
||||
faBars,
|
||||
faArrowLeft,
|
||||
faList,
|
||||
faGear,
|
||||
faTriangleExclamation,
|
||||
faExpand,
|
||||
faTerminal,
|
||||
faArrowRotateLeft,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import {
|
||||
faLightbulb,
|
||||
faCircleQuestion,
|
||||
faCircleRight,
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
import './library'
|
||||
|
||||
import App from './App.vue'
|
||||
import GamePage from './views/GamePage.vue'
|
||||
import AuthPage from './views/AuthPage.vue'
|
||||
|
||||
/* add icons to the library */
|
||||
library.add(
|
||||
faBars,
|
||||
faArrowLeft,
|
||||
faTriangleExclamation,
|
||||
faGear,
|
||||
faExpand,
|
||||
faList,
|
||||
faLightbulb,
|
||||
faCircleQuestion,
|
||||
faArrowRotateLeft,
|
||||
faCircleRight,
|
||||
faTerminal
|
||||
)
|
||||
|
||||
const routes = [
|
||||
{ path: '/', name: 'Home', component: AuthPage },
|
||||
{ path: '/game', name: 'Game', component: GamePage },
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import GamePage from './views/GamePage.vue'
|
||||
import AuthPage from './views/AuthPage.vue'
|
||||
import { isAuthenticated } from './utils/auth'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'auth',
|
||||
component: AuthPage,
|
||||
meta: { requiresGuest: true },
|
||||
},
|
||||
{
|
||||
path: '/game',
|
||||
name: 'game',
|
||||
component: GamePage,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
console.log(to, from)
|
||||
const isAuthed = isAuthenticated.value
|
||||
// Check if the route requires authentication
|
||||
if (to.meta.requiresAuth && !isAuthed) {
|
||||
// Check if the user isn't authenticated
|
||||
// Redirect to the login page
|
||||
next({
|
||||
name: 'auth',
|
||||
query: { redirect: to.fullPath }, // Optionally pass a redirect parameter
|
||||
})
|
||||
} else if (to.meta.requiresGuest && isAuthed) {
|
||||
next({
|
||||
name: 'game', // Or any default authenticated user route
|
||||
})
|
||||
} else {
|
||||
next() // Always call next() at the end
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -2,6 +2,17 @@ import axios from 'axios'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
export const setToken = (token) => {
|
||||
if (token) {
|
||||
api.defaults.headers.common.Authorization = `Bearer ${token}`
|
||||
} else {
|
||||
delete api.defaults.headers.common.Authorization
|
||||
}
|
||||
}
|
||||
|
||||
export default api
|
||||
@@ -0,0 +1,47 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import api, { setToken } from './api'
|
||||
|
||||
const TOKEN_KEY = 'JWT_TOKEN'
|
||||
|
||||
const myStorage = window.localStorage
|
||||
const myToken = ref(myStorage.getItem(TOKEN_KEY))
|
||||
|
||||
if (myToken.value) {
|
||||
setToken(myToken.value)
|
||||
}
|
||||
|
||||
// watch the token and store it in localStorage
|
||||
watch(myToken, (newToken) => {
|
||||
if (newToken) {
|
||||
console.log('update token:', newToken)
|
||||
myStorage.setItem(TOKEN_KEY, newToken)
|
||||
setToken(newToken)
|
||||
} else {
|
||||
myStorage.removeItem(TOKEN_KEY)
|
||||
setToken(null)
|
||||
}
|
||||
})
|
||||
|
||||
export const getToken = () => {
|
||||
return myToken.value
|
||||
}
|
||||
|
||||
export const login = async (username, password) => {
|
||||
try {
|
||||
const res = await api.post('/auth/login', { username, password })
|
||||
const { token } = res.data
|
||||
myToken.value = token
|
||||
return true
|
||||
} catch (err) {
|
||||
if (err.response?.status === 401) {
|
||||
return false
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export const logout = () => {
|
||||
myToken.value = null
|
||||
}
|
||||
|
||||
export const isAuthenticated = computed(() => !!myToken.value)
|
||||
@@ -1,5 +1,7 @@
|
||||
import io from 'socket.io-client'
|
||||
|
||||
import { getToken } from './auth'
|
||||
|
||||
class SocketWrapper {
|
||||
constructor() {
|
||||
this.socket = null
|
||||
@@ -16,6 +18,9 @@ class SocketWrapper {
|
||||
query: {
|
||||
session_id: session._id,
|
||||
},
|
||||
auth: {
|
||||
token: getToken(),
|
||||
},
|
||||
})
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ const router = Router()
|
||||
|
||||
router.post(
|
||||
'/login',
|
||||
passport.authenticate('local', { session: false }),
|
||||
passport.authenticate('local', { session: false, failWithError: true }),
|
||||
issueToken
|
||||
)
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@ router.use('/auth', authRouter)
|
||||
router.use('/quests', quests)
|
||||
router.use(
|
||||
'/sessions',
|
||||
passport.authenticate('jwt', { session: false }),
|
||||
passport.authenticate('jwt', { session: false, failWithError: true }),
|
||||
sessions
|
||||
)
|
||||
router.use(
|
||||
'/commands',
|
||||
passport.authenticate('jwt', { session: false }),
|
||||
passport.authenticate('jwt', { session: false, failWithError: true }),
|
||||
commands
|
||||
)
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ paths:
|
||||
properties:
|
||||
quest_id:
|
||||
type: string
|
||||
example: helloworld
|
||||
responses:
|
||||
'201':
|
||||
description: session created
|
||||
|
||||
Reference in new issue
Block a user