Browse Source

Login should be done. Added application key for JWT signing

master
David Ludwig 4 years ago
parent
commit
b77478d567
12 changed files with 181 additions and 69 deletions
  1. +12
    -2
      .env.example
  2. +5
    -1
      src/app/components/CheckBox.vue
  3. +0
    -2
      src/app/components/TextBox.vue
  4. +40
    -7
      src/app/views/Login.vue
  5. +6
    -6
      src/app/views/Register.vue
  6. +57
    -41
      src/common/validation.ts
  7. +8
    -2
      src/server/Application.ts
  8. +7
    -1
      src/server/index.ts
  9. +1
    -1
      src/server/services/WebServer/index.ts
  10. +22
    -0
      src/server/services/WebServer/requests/LoginRequest.ts
  11. +5
    -5
      src/server/services/WebServer/requests/RegisterRequest.ts
  12. +18
    -1
      src/server/services/WebServer/routes/auth.ts

+ 12
- 2
.env.example View File

@ -1,3 +1,7 @@
# Application key to sign stuff
APP_KEY_FILE = /run/secrets/app_key
# Database credentials
DB_TYPE = mysql
DB_HOST = database
DB_PORT = 3306
@ -5,7 +9,13 @@ DB_USER = root
DB_PASSWORD_FILE = /run/secrets/mysql_root_password
DB_DATABASE = autoplex_request
SERVER_PORT = 3200
# Interfaces ---------------------------------------------------------------------------------------
# Torrent client IPC socket path
TORRENT_CLIENT_IPC_SOCKET = /tmp/torrent_client.sock
DISCORD_BOT_TOKEN=
# Web server port
WEBSERVER_PORT = 3200
# Discord bot token
DISCORD_BOT_TOKEN =

+ 5
- 1
src/app/components/CheckBox.vue View File

@ -1,6 +1,6 @@
<template>
<label class="checkbox-label relative flex items-center">
<input type="checkbox">
<input type="checkbox" :disabled="disabled">
<span></span>
<span v-if="label" class="ml-2">{{label}}</span>
</label>
@ -11,6 +11,10 @@ import { defineComponent } from "vue";
export default defineComponent({
props: {
disabled: {
type: Boolean,
default: false
},
label: {
type: String,
required: true


+ 0
- 2
src/app/components/TextBox.vue View File

@ -73,8 +73,6 @@ export default defineComponent({
if (!this.error) {
this.isValid = true;
}
} else {
this.isValid = true;
}
}
},


+ 40
- 7
src/app/views/Login.vue View File

@ -2,19 +2,21 @@
<div class="w-full sm:max-w-sm sm:mx-auto sm:my-auto p-6 space-y-4 bg-white rounded-lg shadow-md">
<div class="text-center font-thin text-4xl py-4">AUTOPLEX</div>
<div class="font-medium text-center text-xl">Sign in</div>
<form>
<form @submit.prevent="login">
<div class="space-y-4">
<div>
<text-box label="Email" type="email" placeholder="john@example.com" :error-message="errors.email"/>
<text-box label="Email" type="email" ref="email" placeholder="john@example.com" :disabled="isSubmitting"
v-model="fields.email"/>
</div>
<div>
<text-box label="Password" type="password" placeholder="············" :error-message="errors.password"/>
<text-box label="Password" type="password" ref="password" placeholder="············" :disabled="isSubmitting"
v-model="fields.password"/>
</div>
<div>
<check-box label="Remember Me"/>
<check-box label="Remember Me" :disabled="isSubmitting"/>
</div>
<div>
<button @click="login" class="block w-full rounded-full bg-indigo-500 text-white p-2 focus:outline-none">Sign In</button>
<button @click="login" :disabled="isSubmitting" class="block w-full rounded-full bg-indigo-500 text-white p-2 focus:outline-none">Sign In</button>
</div>
</div>
</form>
@ -33,7 +35,8 @@ export default defineComponent({
},
data() {
return {
errors: {
isSubmitting: false,
fields: {
email: "",
password: ""
}
@ -41,7 +44,37 @@ export default defineComponent({
},
methods: {
login() {
if (this.isSubmitting) {
return;
}
this.isSubmitting = true;
fetch("/auth/login", {
method: "post",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(this.fields)
})
.then(async response => {
this.isSubmitting = false;
let body = await response.json();
console.log("The response is:", response.status);
if (response.status !== 200) {
if (body.errors) {
for (let fieldName in this.fields) {
let field = <any>this.$refs[fieldName];
let message = <string>(body.errors[fieldName] ?? [""])[0];
field.setErrorMessage(message);
}
}
return;
}
this.$router.push({ name: "Login" });
})
.catch(e => {
console.error("Error occurred during submission:", e);
this.isSubmitting = false;
});
}
}
});


+ 6
- 6
src/app/views/Register.vue View File

@ -56,9 +56,9 @@ export default defineComponent({
retypePassword: "",
}),
constraints: {
name: constraints.name,
email: constraints.email,
password: constraints.password
name: constraints.register.name,
email: constraints.register.email,
password: constraints.register.password
}
}
},
@ -113,7 +113,7 @@ export default defineComponent({
* Validate the provided email address field
*/
async validateEmail(email: string) {
let error = validateValue(email, constraints.email);
let error = validateValue(email, constraints.register.email);
if (error) {
return error;
}
@ -129,10 +129,10 @@ export default defineComponent({
*/
validateRetypePassword(value: string) {
if (value.trim().length == 0) {
return constraints.retypePassword.presence.message;
return constraints.register.retypePassword.presence.message;
}
if (value !== this.fields.password) {
return constraints.retypePassword.equality.message;
return constraints.register.retypePassword.equality.message;
}
}
},


+ 57
- 41
src/common/validation.ts View File

@ -1,53 +1,69 @@
export const constraints = {
token: {
presence: {
message: "A valid token is required to register"
},
token: {
message: "A valid token is required to register"
}
},
name: {
presence: {
allowEmpty: false,
message: "Your name is required"
login: {
email: {
presence: {
allowEmpty: false,
message: "An email address is required"
}
},
length: {
maximum: 50,
tooLong: "Your name cannot exceed 50 characters"
password: {
presence: {
allowEmpty: false,
message: "A password is required"
}
}
},
email: {
presence: {
allowEmpty: false,
message: "Your email is required"
register: {
token: {
presence: {
message: "A valid token is required to register"
},
token: {
message: "A valid token is required to register"
}
},
length: {
maximum: 255,
tooLong: "An email address cannot exceed 255 characters"
name: {
presence: {
allowEmpty: false,
message: "Your name is required"
},
length: {
maximum: 50,
tooLong: "Your name cannot exceed 50 characters"
}
},
email: {
message: "A valid email address is required"
}
},
password: {
presence: {
allowEmpty: false,
message: "A password is required"
presence: {
allowEmpty: false,
message: "Your email is required"
},
length: {
maximum: 255,
tooLong: "An email address cannot exceed 255 characters"
},
email: {
message: "A valid email address is required"
}
},
length: {
minimum: 8,
tooShort: "Password should be at least 8 characters"
}
},
retypePassword: {
presence: {
allowEmpty: false,
message: "Re-type your password to confirm it"
password: {
presence: {
allowEmpty: false,
message: "A password is required"
},
length: {
minimum: 8,
tooShort: "Password should be at least 8 characters"
}
},
equality: {
attribute: "password",
message: "Passwords must match"
retypePassword: {
presence: {
allowEmpty: false,
message: "Re-type your password to confirm it"
},
equality: {
attribute: "password",
message: "Passwords must match"
}
}
}
};

+ 8
- 2
src/server/Application.ts View File

@ -23,6 +23,11 @@ async function createRegisterToken() {
*/
export default class Application
{
/**
* The application key used for signing stuff
*/
public readonly APP_KEY: string;
/**
* All available services
*/
@ -51,7 +56,8 @@ export default class Application
/**
* Create a new application instance
*/
public constructor() {
public constructor(appKey: string) {
this.APP_KEY = appKey;
this.services = [
this.database = new Database(this),
// this.discord = new DiscordBot(this),
@ -63,7 +69,7 @@ export default class Application
}
/**
* Boot all of the services
* Boot the application and all of the services
*/
protected async boot() {
return Promise.all(this.services.map(service => service.boot()));


+ 7
- 1
src/server/index.ts View File

@ -1,9 +1,15 @@
import { readFileSync } from "fs";
import Application from "./Application";
/**
* Load the application key
*/
let appKey = readFileSync(<string>process.env["APP_KEY_FILE"]).toString();
/**
* Create a new application instance
*/
let app = new Application();
let app = new Application(appKey);
/**
* Start the application


+ 1
- 1
src/server/services/WebServer/index.ts View File

@ -26,7 +26,7 @@ export default class WebServer extends Service
*/
public constructor(app: Application) {
super("Web Server", app);
this.port = parseInt(<string>process.env["SERVER_PORT"]);
this.port = parseInt(<string>process.env["WEBSERVER_PORT"]);
this.server = fastify();
this.registerPlugins();
}


+ 22
- 0
src/server/services/WebServer/requests/LoginRequest.ts View File

@ -0,0 +1,22 @@
import { FastifyRequest } from "fastify";
import validate from "validate.js";
import { constraints } from "@common/validation";
import Request from "./Request";
export interface ILoginFormBody {
email: string,
password: string
}
export default class LoginRequest extends Request
{
/**
* Validate the request
*/
public validate(request: FastifyRequest) {
return validate.async(request.body, {
email: constraints.login.email,
password: constraints.login.password,
},<any>{ fullMessages: false });
}
}

+ 5
- 5
src/server/services/WebServer/requests/RegisterRequest.ts View File

@ -18,11 +18,11 @@ export default class RegisterRequest extends Request
*/
public validate(request: FastifyRequest) {
return validate.async(request.body, {
token: constraints.token,
name: constraints.name,
email: constraints.email,
password: constraints.password,
retypePassword: constraints.retypePassword
token: constraints.register.token,
name: constraints.register.name,
email: constraints.register.email,
password: constraints.register.password,
retypePassword: constraints.register.retypePassword
},<any>{ fullMessages: false });
}
}

+ 18
- 1
src/server/services/WebServer/routes/auth.ts View File

@ -2,14 +2,30 @@ import Application from "@server/Application";
import { FastifyInstance } from "fastify";
import bcrypt from "bcrypt";
import { RegisterToken, User } from "@server/database/entities";
import RegisterRequest, {IRegisterFormBody} from "../requests/RegisterRequest";
import LoginRequest, { ILoginFormBody } from "../requests/LoginRequest";
import RegisterRequest, { IRegisterFormBody } from "../requests/RegisterRequest";
import handle from "../requests";
import jwt from "jsonwebtoken";
/**
* Register authentication routes
*/
export default function register(server: FastifyInstance, app: Application) {
// Login ---------------------------------------------------------------------------------------
server.post("/auth/login", handle([LoginRequest], async (request, reply) => {
let body = <ILoginFormBody>request.body;
let user = await User.findOne({ email: body.email });
if (user === undefined || !(await bcrypt.compare(body.password, user.password))) {
reply.status(401);
reply.send({ "status": "unauthorized" });
return
}
let token = jwt.sign({ id: (<User>user).id }, app.APP_KEY, { expiresIn: 60*60*24 });
reply.send({ "status": "success" });
}));
// Registration --------------------------------------------------------------------------------
/**
@ -23,6 +39,7 @@ export default function register(server: FastifyInstance, app: Application) {
user.email = body.email.trim();
user.password = await bcrypt.hash(body.password, 8);
await user.save();
await RegisterToken.delete({token: body.token });
reply.send({ status: "success" });
}));


Loading…
Cancel
Save