Skip to content

Commit

Permalink
feat: admin login to use admin table (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
Teddy-Schmitz authored Oct 3, 2023
1 parent b8d0230 commit 42f236a
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 86 deletions.
2 changes: 1 addition & 1 deletion dockerfile → Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ RUN apt-get update && apt-get upgrade -y && apt-get autoclean -y && apt-get auto
RUN groupadd -r nodejs && useradd -g nodejs -s /bin/bash -d /home/nodejs -m nodejs
USER nodejs
# set right (secure) folder permissions
RUN mkdir -p /home/nodejs/app/node_modules && chown -R nodejs:nodejs /home/nodejs/app
RUN mkdir -p /home/nodejs/app/node_modules /home/nodejs/app/authdata && chown -R nodejs:nodejs /home/nodejs/app

WORKDIR /home/nodejs/app

Expand Down
34 changes: 34 additions & 0 deletions client/admin/loginAdminPage.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,39 @@
Welcome, sign in with your admin credentials.
</p>

<div class="my-4">
<label for="email" class="sr-only">Email</label>

<div class="relative">
<input
type="email"
class="w-full p-4 text-sm border-gray-200 rounded-lg shadow-sm pe-12"
type="email"
v-model="email"
placeholder="Enter email"
/>

<span
class="absolute inset-y-0 grid px-4 end-0 place-content-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"
/>
</svg>
</span>
</div>
</div>

<div class="my-4">
<label for="password" class="sr-only">Password</label>

Expand Down Expand Up @@ -183,6 +216,7 @@
data: {
type: "users",
attributes: {
email: email.value,
password: password.value,
},
},
Expand Down
18 changes: 18 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: '3'

services:
authcompanion:
build: .
container_name: authcompanion
environment:
- DB_PATH=./authdata/authcompanion_users.db
- ADMIN_KEY_PATH=./authdata/adminkey
- KEY_PATH=./authdata/serverkey
ports:
- "3002:3002"
volumes:
- authdata:/home/nodejs/app/authdata


volumes:
authdata:
2 changes: 1 addition & 1 deletion env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ ADMIN_ORIGIN=http://localhost:3002/v1/admin/dashboard

#------------------- DB Settings ---------------------#
## DB URI or path to SQLite file.
DB_PATH=./users_authc.db
DB_PATH=./authcompanion_users.db

#------------------- JWT Secrets ----------------------#
## Path to the JSON Web Key format key used to sign
Expand Down
61 changes: 40 additions & 21 deletions plugins/db/db.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,67 @@
import { readFileSync, existsSync } from "fs";
import Database from "better-sqlite3";
import config from "../../config.js";
import fastifyPlugin from "fastify-plugin";
import { readFileSync, existsSync, readdirSync } from 'fs';
import { extname } from 'path';
import Database from 'better-sqlite3';
import config from '../../config.js';
import fastifyPlugin from 'fastify-plugin';

const VERSION = 2;

const migrate = (db, version) => {
const allFiles = readdirSync('./plugins/db/schema/');
const sqlFiles = allFiles.filter((file) => extname(file) === '.sql');
sqlFiles.sort();
if (version === null || version === undefined) {
version = 1;
}

for (const sqlFile of sqlFiles) {
if (!sqlFile.includes(`${version}`)) {
continue;
}
console.log(`Migrating to ${sqlFile}`);
const migration = readFileSync(`./plugins/db/schema/${sqlFile}`, 'utf8');
db.exec(migration);
version++;
}
};

const dbPlugin = async function (fastify) {
let db = {};
try {
//create test database to support CI
if (process.env.NODE_ENV === "test") {
config.DBPATH = "./test.db";
console.log("Test database - ENABLED");
if (process.env.NODE_ENV === 'test') {
config.DBPATH = './test.db';
console.log('Test database - ENABLED');
}

//make sure the database is available, if not, create one
if (!existsSync(config.DBPATH)) {
//create database if it does not exist and migrate
db = new Database(config.DBPATH);
db.pragma("journal_mode = WAL");

const migration = readFileSync("./plugins/db/1__main.sql", "utf8");
db.exec(migration);

db.pragma('journal_mode = WAL');
migrate(db, 1);
fastify.log.info(`Generated Sqlite3 Database: ${config.DBPATH}...`);
} else {
db = new Database(config.DBPATH);
db.pragma("journal_mode = WAL");
db.pragma('journal_mode = WAL');

//if the database is available, make sure it has the right schema
const stmt = db.prepare("SELECT version from authc_version");
const stmt = db.prepare('SELECT version from authc_version');
const { version } = stmt.get();

if (!version || version !== 1) {
throw new Error("Database is an unexpected version, please try again");
if (!version || version > VERSION) {
throw new Error('Database is an unexpected version, please try again');
} else if (version < VERSION) {
migrate(db, VERSION);
}
}
fastify.log.info(`Using Sqlite3 Database: ${config.DBPATH}`);
} catch (error) {
console.log(error);
throw new Error(
"There was an error setting and connecting up the Database, please try again!"
);
throw new Error('There was an error setting and connecting up the Database, please try again!');
}
//make available the database across the server by calling "db"
fastify.decorate("db", db);
fastify.decorate('db', db);
};
//wrap the function with the fastly plugin to expose outside of the registered scope
export default fastifyPlugin(dbPlugin, { fastify: "4.x" });
export default fastifyPlugin(dbPlugin, { fastify: '4.x' });
File renamed without changes.
17 changes: 17 additions & 0 deletions plugins/db/schema/2__admin.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
BEGIN TRANSACTION;
CREATE TABLE admin (
id INTEGER PRIMARY KEY ASC,
uuid TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
challenge TEXT,
jwt_id TEXT NOT NULL,
active INTEGER NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);

UPDATE authc_version SET version = 2 WHERE version = 1;

COMMIT TRANSACTION;
52 changes: 22 additions & 30 deletions plugins/key/admin.key.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import fastifyPlugin from "fastify-plugin";
import config from "../../config.js";
import { writeFileSync } from "fs";
import { createId } from "@paralleldrive/cuid2";
import { createHash } from "../../utils/credential.js";
import * as crypto from "crypto";
import fastifyPlugin from 'fastify-plugin';
import config from '../../config.js';
import { writeFileSync } from 'fs';
import { createId } from '@paralleldrive/cuid2';
import { createHash } from '../../utils/credential.js';
import * as crypto from 'crypto';

//function to generate a random password using crypto module
const generatePassword = function () {
return crypto.randomBytes(20).toString("hex");
return crypto.randomBytes(20).toString('hex');
};

const setupAdminKey = async function (fastify) {
try {
//create test database to support CI
if (process.env.NODE_ENV === "test") {
config.ADMINKEYPATH = "./adminkey_test";
if (process.env.NODE_ENV === 'test') {
config.ADMINKEYPATH = './adminkey_test';
}

//Check if the admin user already exists on server startup
const stmt = fastify.db.prepare(
"SELECT uuid, name, email, active, created_at, updated_at FROM users WHERE email = ?;"
);
const adminUser = await stmt.get("admin@localhost");
const stmt = fastify.db.prepare('SELECT uuid, name, email, active, created_at, updated_at FROM admin LIMIT 1;');
const adminUser = await stmt.get();

if (adminUser) {
//register the admin user on the fastify instance
fastify.decorate("registeredAdminUser", adminUser);
fastify.decorate('registeredAdminUser', adminUser);
fastify.log.info(`Using Admin API Key: ${config.ADMINKEYPATH}`);
return;
}
Expand All @@ -35,38 +33,32 @@ const setupAdminKey = async function (fastify) {
const adminPwd = generatePassword();
const hashPwd = await createHash(adminPwd);

//Create a user object to use to create the user in the database and access token
//Create a default admin account
const userObj = {
uuid: uuid,
name: "Admin",
email: "admin@localhost",
name: 'Admin',
email: 'admin@localhost',
};

const registerStmt = fastify.db.prepare(
"INSERT INTO users (uuid, name, email, password, active, jwt_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'), strftime('%Y-%m-%dT%H:%M:%fZ','now')) RETURNING uuid, name, email, active, created_at, updated_at;"
);
const user = registerStmt.get(
uuid,
userObj.name,
userObj.email,
hashPwd,
1,
""
"INSERT INTO admin (uuid, name, email, password, active, jwt_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'), strftime('%Y-%m-%dT%H:%M:%fZ','now')) RETURNING uuid, name, email, active, created_at, updated_at;"
);
const user = registerStmt.get(uuid, userObj.name, userObj.email, hashPwd, 1, '');

//export admin password to a file. Admin password is only exported once on server startup and can be traded for an access token
writeFileSync(config.ADMINKEYPATH, `admin password: ${adminPwd}`);
fastify.log.info(`Generating Admin API Key: ${config.KEYPATH}...`);
fastify.log.info(`Generating Admin API Key: ${config.ADMINKEYPATH}...`);
fastify.log.info(`Admin Password is: ${adminPwd}`);

//register the admin user on the fastify instance
fastify.decorate("registeredAdminUser", user);
fastify.decorate('registeredAdminUser', user);

fastify.log.info(`Using Admin API Key: ${config.ADMINKEYPATH}`);
} catch (error) {
console.log(error);
throw new Error("Failed to export the admin key");
throw new Error('Failed to export the admin key');
}
};

//Wrap as Fastify Plugin
export default fastifyPlugin(setupAdminKey, { fastify: "4.x" });
export default fastifyPlugin(setupAdminKey, { fastify: '4.x' });
43 changes: 18 additions & 25 deletions services/admin/users/login.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,39 @@
import { verifyValueWithHash } from "../../../utils/credential.js";
import { makeAdminToken } from "../../../utils/jwt.js";
import config from "../../../config.js";
import { verifyValueWithHash } from '../../../utils/credential.js';
import { makeAdminToken } from '../../../utils/jwt.js';
import config from '../../../config.js';

export const loginHandler = async function (request, reply) {
try {
// Check the request's type attibute is set to users
if (request.body.data.type !== "users") {
request.log.info(
"Auth API: The request's type is not set to Users, creation failed"
);
throw { statusCode: 400, message: "Invalid Type Attribute" };
if (request.body.data.type !== 'users') {
request.log.info("Auth API: The request's type is not set to Users, creation failed");
throw { statusCode: 400, message: 'Invalid Type Attribute' };
}

// Fetch the registered admin user from the database with
const stmt = this.db.prepare(
"SELECT uuid, name, email, jwt_id, password, active, created_at, updated_at FROM users WHERE uuid = ?;"
'SELECT uuid, name, email, jwt_id, password, active, created_at, updated_at FROM admin WHERE email = ?;'
);
const userObj = await stmt.get(this.registeredAdminUser.uuid);
const userObj = await stmt.get(request.body.data.attributes.email);

// Check if admin user does not exist in the database
if (!userObj) {
request.log.info(
"Auth API: User does not exist in database, login failed"
);
throw { statusCode: 400, message: "Login Failed" };
request.log.info('Auth API: User does not exist in database, login failed');
throw { statusCode: 400, message: 'Login Failed' };
}

// Check if user has an 'active' account
if (!userObj.active) {
request.log.info("Auth API: User account is not active, login failed");
throw { statusCode: 400, message: "Login Failed" };
request.log.info('Auth API: User account is not active, login failed');
throw { statusCode: 400, message: 'Login Failed' };
}

const passwordCheckResult = await verifyValueWithHash(
request.body.data.attributes.password,
userObj.password
);
const passwordCheckResult = await verifyValueWithHash(request.body.data.attributes.password, userObj.password);

// Check if user has the correct password
if (!passwordCheckResult) {
request.log.info("Auth API: User password is incorrect, login failed");
throw { statusCode: 400, message: "Login Failed" };
request.log.info('Auth API: User password is incorrect, login failed');
throw { statusCode: 400, message: 'Login Failed' };
}

// Looks good! Let's prepare the reply
Expand All @@ -57,16 +50,16 @@ export const loginHandler = async function (request, reply) {
expireDate.setTime(expireDate.getTime() + 7 * 24 * 60 * 60 * 1000); // TODO: Make configurable now, set to 7 days

reply.headers({
"set-cookie": [
'set-cookie': [
`adminAccessToken=${adminAccessToken.token}; Path=/; Expires=${expireDate}; SameSite=None; Secure; HttpOnly`,
`Fgp=${adminAccessToken.userFingerprint}; Path=/; Max-Age=7200; SameSite=None; Secure; HttpOnly`,
],
"x-authc-app-origin": config.ADMINORIGIN,
'x-authc-app-origin': config.ADMINORIGIN,
});

return {
data: {
type: "users",
type: 'users',
id: userObj.uuid,
attributes: userAttributes,
},
Expand Down
Loading

0 comments on commit 42f236a

Please sign in to comment.