From 16405d84ae9c3ca57e0a06f2d467d5be5b68a079 Mon Sep 17 00:00:00 2001 From: David Ludwig Date: Wed, 14 Apr 2021 00:58:40 -0500 Subject: [PATCH] Client side authentication now being handled --- src/app/App.vue | 8 ++- src/app/auth.ts | 62 ++++++++++++++++++-- src/app/routes/index.ts | 14 +++++ src/app/views/Error404.vue | 6 ++ src/app/views/Login.vue | 7 ++- src/server/database/entities/User.ts | 22 +++++++ src/server/services/WebServer/routes/auth.ts | 7 +-- 7 files changed, 113 insertions(+), 13 deletions(-) create mode 100644 src/app/views/Error404.vue diff --git a/src/app/App.vue b/src/app/App.vue index fc9563e..f24d015 100644 --- a/src/app/App.vue +++ b/src/app/App.vue @@ -6,5 +6,11 @@ diff --git a/src/app/auth.ts b/src/app/auth.ts index b95cff9..b1fb896 100644 --- a/src/app/auth.ts +++ b/src/app/auth.ts @@ -1,19 +1,71 @@ + import jwtDecode from "jwt-decode"; -let token: string | null; +/** + * The active JWT + */ + let token: string | null; + +/** + * The decoded user object + */ +let user: { + id: number, + name: string, + isAdmin: boolean +} | null; + +/** + * Check if the user is an admin + */ +export function isAdmin() { + return user && user.isAdmin; +} +/** + * Check if the client is authenticated + */ export function isAuthenticated() { return Boolean(token); } -export function decodeToken(token: string) { - return jwtDecode(token); +/** + * Load the token from local storage + */ +export function loadToken() { + try { + token = localStorage.getItem("jwt"); + user = jwtDecode(token); + } catch(e) { + console.log("Failed to load token"); + token = null; + user = null; + return false; + } + return true; } +/** + * Delete the token from local storage + */ export function forgetToken() { + token = null; + user = null; localStorage.removeItem("jwt"); } -export function storeToken(token: string) { - localStorage.setItem("jwt", token); +/** + * Store a JWT token in local storage + */ +export function storeToken(jwtToken: string) { + try { + user = jwtDecode(jwtToken); + token = jwtToken; + localStorage.setItem("jwt", jwtToken); + } catch(e) { + user = null; + token = null; + return false; + } + return true; } diff --git a/src/app/routes/index.ts b/src/app/routes/index.ts index 40e361c..062c6a2 100644 --- a/src/app/routes/index.ts +++ b/src/app/routes/index.ts @@ -41,6 +41,20 @@ const routes: RouteRecordRaw[] = [ name: "Register", component: () => import("../views/Register.vue"), beforeEnter: requiresGuest + }, + { + path: "/logout", + name: "Logout", + component: { + beforeRouteEnter(to, from, next) { + auth.forgetToken(); + next({ name: "Login" }); + } + } + }, + { + path: "/:pathMatch(.*)*", + component: () => import("../views/Error404.vue") } ]; diff --git a/src/app/views/Error404.vue b/src/app/views/Error404.vue new file mode 100644 index 0000000..225add0 --- /dev/null +++ b/src/app/views/Error404.vue @@ -0,0 +1,6 @@ + + diff --git a/src/app/views/Login.vue b/src/app/views/Login.vue index 772c896..011736e 100644 --- a/src/app/views/Login.vue +++ b/src/app/views/Login.vue @@ -59,8 +59,10 @@ export default defineComponent({ .then(async response => { this.isSubmitting = false; let body = await response.json(); - console.log("The response is:", response.status); if (response.status !== 200) { + if (response.status === 401) { + body.errors = { "email": [ "Email or password is incorrect" ] }; + } if (body.errors) { for (let fieldName in this.fields) { let field = this.$refs[fieldName]; @@ -70,9 +72,8 @@ export default defineComponent({ } return; } - console.log("Successful login", body.token); auth.storeToken(body.token); - this.$router.push({ name: "Login" }); + this.$router.push({ name: "Home" }); }) .catch(e => { console.error("Error occurred during submission:", e); diff --git a/src/server/database/entities/User.ts b/src/server/database/entities/User.ts index bbdcbde..6f68d74 100644 --- a/src/server/database/entities/User.ts +++ b/src/server/database/entities/User.ts @@ -1,5 +1,8 @@ import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, OneToMany } from "typeorm"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; import { MovieTicket } from "./MovieTicket"; +import Application from "@server/Application"; @Entity() export class User extends BaseEntity @@ -21,4 +24,23 @@ export class User extends BaseEntity @OneToMany(() => User, user => user.movieTickets) movieTickets!: MovieTicket[]; + + /** + * Authenticate a user and return an auth token upon success + */ + public static async authenticate(email: string, password: string) { + let user = await User.findOne({ email }); + if (user === undefined || !(await bcrypt.compare(password, user.password))) { + return undefined; + } + return user.createJwtToken(Application.instance().APP_KEY); + } + + /** + * Create an auth token for the user + */ + public createJwtToken(key: string, expiresIn: number = 60*60*24) { + let body = { id: this.id, name: this.name, isAdmin: this.isAdmin }; + return jwt.sign(body, key, { expiresIn }); + } } diff --git a/src/server/services/WebServer/routes/auth.ts b/src/server/services/WebServer/routes/auth.ts index abbf933..621f371 100644 --- a/src/server/services/WebServer/routes/auth.ts +++ b/src/server/services/WebServer/routes/auth.ts @@ -23,20 +23,19 @@ export default function register(factory: RouteRegisterFactory, app: Application factory.post("/auth/login", handle([LoginRequest], async (request, reply) => { let body = request.body; - let user = await User.findOne({ email: body.email }); - if (user === undefined || !(await bcrypt.compare(body.password, user.password))) { + let token = await User.authenticate(body.email, body.password); + if (token === undefined) { reply.status(401); reply.send({ "status": "unauthorized" }); return } - let token = jwt.sign({ id: (user).id }, app.APP_KEY, { expiresIn: 60*60*24 }); + // Store the header/payload in the client, store the signature in a secure httpOnly cookie let [header, payload, signature] = token.split('.'); reply.setCookie("jwt_signature", signature, { httpOnly: true, sameSite: true, secure: true }); - console.log(signature); reply.send({ status: "success", token: `${header}.${payload}` }); }));