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:
Lin, Hung-Shin committed 2023-09-13 14:02:28 +08:00
1 parent b55b7aa1ec
commit 49417c405a
11 files changed
+206 -61

No files matched your search

+37 -7
View File
@@ -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>
+17 -1
View File
@@ -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>
+37
View File
@@ -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
View File
@@ -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)
+46
View File
@@ -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
+11
View File
@@ -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
+47
View File
@@ -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)
+5
View File
@@ -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', () => {
+1 -1
View File
@@ -11,7 +11,7 @@ const router = Router()
router.post(
'/login',
passport.authenticate('local', { session: false }),
passport.authenticate('local', { session: false, failWithError: true }),
issueToken
)
+2 -2
View File
@@ -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
)
+1
View File
@@ -62,6 +62,7 @@ paths:
properties:
quest_id:
type: string
example: helloworld
responses:
'201':
description: session created