mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-08 11:30:12 +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>
|
<template>
|
||||||
<!--login page-->
|
<!--login page-->
|
||||||
<div
|
<div
|
||||||
class="bg-background border-8 border-background-primary flex flex-1 flex-col items-center justify-center rounded-3xl p-10"
|
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>
|
<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">
|
<div class="mb-6">
|
||||||
<label for="email" class="text-sm font-semibold text-text">Email</label
|
<label for="email" class="text-sm font-semibold text-text">Email</label
|
||||||
><input
|
><input
|
||||||
@@ -13,6 +38,8 @@
|
|||||||
id="email"
|
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"
|
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"
|
placeholder="Enter your email address"
|
||||||
|
v-model="username"
|
||||||
|
@input="clearError"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
@@ -23,16 +50,19 @@
|
|||||||
id="password"
|
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"
|
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"
|
placeholder="Enter your password"
|
||||||
|
v-model="password"
|
||||||
|
@input="clearError"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<p class="text-red-500 flex justify-center" v-if="errorMessage">
|
||||||
type="submit"
|
{{ 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"
|
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>
|
<span>Log in / Sign up</span>
|
||||||
</router-link>
|
</button>
|
||||||
<input type="hidden" name="remember" value="true" />
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<template>
|
||||||
<div class="h-full flex items-center justify-center mx-3">
|
<div class="h-full flex items-center justify-center mx-3">
|
||||||
@@ -46,6 +56,12 @@
|
|||||||
class="text-text-primary h-5 w-5"
|
class="text-text-primary h-5 w-5"
|
||||||
/>
|
/>
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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 { createApp } from 'vue'
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
|
||||||
import './style.css'
|
import './style.css'
|
||||||
|
|
||||||
/* import the fontawesome core */
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
|
||||||
|
|
||||||
/* import font awesome icon component */
|
/* import font awesome icon component */
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
import './library'
|
||||||
/* 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 App from './App.vue'
|
import App from './App.vue'
|
||||||
import GamePage from './views/GamePage.vue'
|
import router from './router'
|
||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
const app = createApp(App)
|
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({
|
const api = axios.create({
|
||||||
baseURL: '/api/v1',
|
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
|
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 io from 'socket.io-client'
|
||||||
|
|
||||||
|
import { getToken } from './auth'
|
||||||
|
|
||||||
class SocketWrapper {
|
class SocketWrapper {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.socket = null
|
this.socket = null
|
||||||
@@ -16,6 +18,9 @@ class SocketWrapper {
|
|||||||
query: {
|
query: {
|
||||||
session_id: session._id,
|
session_id: session._id,
|
||||||
},
|
},
|
||||||
|
auth: {
|
||||||
|
token: getToken(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
this.socket.on('connect', () => {
|
this.socket.on('connect', () => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const router = Router()
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/login',
|
'/login',
|
||||||
passport.authenticate('local', { session: false }),
|
passport.authenticate('local', { session: false, failWithError: true }),
|
||||||
issueToken
|
issueToken
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ router.use('/auth', authRouter)
|
|||||||
router.use('/quests', quests)
|
router.use('/quests', quests)
|
||||||
router.use(
|
router.use(
|
||||||
'/sessions',
|
'/sessions',
|
||||||
passport.authenticate('jwt', { session: false }),
|
passport.authenticate('jwt', { session: false, failWithError: true }),
|
||||||
sessions
|
sessions
|
||||||
)
|
)
|
||||||
router.use(
|
router.use(
|
||||||
'/commands',
|
'/commands',
|
||||||
passport.authenticate('jwt', { session: false }),
|
passport.authenticate('jwt', { session: false, failWithError: true }),
|
||||||
commands
|
commands
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
quest_id:
|
quest_id:
|
||||||
type: string
|
type: string
|
||||||
|
example: helloworld
|
||||||
responses:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
description: session created
|
description: session created
|
||||||
|
|||||||
Reference in new issue
Block a user