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 @@
+
+
+
404
+
+
+
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}` });
}));