Browse Source

save: try remove password env by setup at first launch

pull/1228/head
tetuaoro 2 years ago
parent
commit
29cf2e2616
  1. 11
      .vscode/settings.json
  2. 33
      src/lib/Server.js
  3. 42
      src/lib/database/AbstractDatabase.js
  4. 80
      src/lib/database/Lowdb.js
  5. 85
      src/templates/Setup.vue
  6. 25
      src/www/css/app.css
  7. 1103
      src/www/index.html
  8. 23
      src/www/js/api.js
  9. 8
      src/www/js/app.js
  10. 4
      src/www/js/i18n.js
  11. 2
      src/www/js/vendor/vue-setup.umd.min.js
  12. 1
      src/www/js/vendor/vue-setup.umd.min.js.map

11
.vscode/settings.json

@ -0,0 +1,11 @@
{
"[javascript]": {
"editor.formatOnSave": true,
"editor.formatOnType": true
},
"javascript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": false,
"javascript.format.semicolons": "insert",
"editor.tabSize": 2,
"editor.indentSize": "tabSize",
"editor.detectIndentation": false
}

33
src/lib/Server.js

@ -129,7 +129,7 @@ module.exports = class Server {
// WireGuard // WireGuard
app.use( app.use(
fromNodeMiddleware(async (req, res, next) => { fromNodeMiddleware(async (req, res, next) => {
if (!requiresPassword || !req.url.startsWith('/api/')) { if (!req.url.startsWith('/api/')) {
return next(); return next();
} }
@ -273,12 +273,33 @@ module.exports = class Server {
return { success: true }; return { success: true };
})); }));
// setting // Setup
const routerSetting = createRouter(); const routerSetup = createRouter();
app.use(routerSetting); app.use(routerSetup);
routerSetting routerSetup
.put('/api/setting/password', defineEventHandler(async (event) => { .get('/setup', defineEventHandler((_event) => {
const isFirstSetup = Database.firstSetupAuth();
return { success: isFirstSetup };
}))
.post('/setup/user', defineEventHandler(async (event) => {
try {
const { username, password } = await readBody(event);
await Database.addAdminUser(username, password);
return { success: true };
} catch (error) {
return createError({
status: error.statusCode,
data: { errorMessage: error.message || error.toString() },
});
}
}))
.post('/api/setup/user', defineEventHandler(async (event) => {
const { username, password } = await readBody(event);
await Database.addUser(username, password);
return { success: true };
}))
.put('/api/setup/user', defineEventHandler(async (event) => {
const { username, oldPassword, newPassword } = await readBody(event); const { username, oldPassword, newPassword } = await readBody(event);
await Database.updatePassword(username, oldPassword, newPassword); await Database.updatePassword(username, oldPassword, newPassword);
return { success: true }; return { success: true };

42
src/lib/database/AbstractDatabase.js

@ -2,6 +2,12 @@
const ServerError = require('../ServerError'); const ServerError = require('../ServerError');
/**
* All database implementations must extend this class and implement the following methods
*
* @abstract
*
*/
module.exports = class DatabaseInterface { module.exports = class DatabaseInterface {
constructor() { constructor() {
@ -10,9 +16,26 @@ module.exports = class DatabaseInterface {
} }
} }
/**
* Checks if the password meets complexity requirements
*
* @param {string} password - The password to check
* @returns {boolean} True if the password is complex enough, otherwise false
*/
isPasswordComplex(password) {
const minLength = 12;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumber = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
return password.length >= minLength && hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar;
}
/** /**
* Initializes the database if does not exist * Initializes the database if does not exist
* *
* @abstract
* @returns {Promise<void>} * @returns {Promise<void>}
* @throws {ServerError} If not implemented by the subclass * @throws {ServerError} If not implemented by the subclass
*/ */
@ -23,23 +46,40 @@ module.exports = class DatabaseInterface {
/** /**
* Compares the provided password with the stored password for the given username * Compares the provided password with the stored password for the given username
* *
* @abstract
* @param {string} username - The username of the user * @param {string} username - The username of the user
* @param {string} password - The password to compare * @param {string} password - The password to compare
* @returns {Promise<boolean>} - Return `true` if the password matches, otherwise `false` * @returns {Promise<boolean>} Return `true` if the password matches, otherwise `false`
* @throws {ServerError} If not implemented by the subclass * @throws {ServerError} If not implemented by the subclass
*/ */
async comparePassword(username, password) { async comparePassword(username, password) {
throw new ServerError('You must implement this function'); throw new ServerError('You must implement this function');
} }
/**
* Adds a new user to the database.
*
* @abstract
* @param {string} username - The username of the new user
* @param {string} password - The password of the new user
* @returns {Promise<void>}
* @throws {ServerError} If not implemented by the subclass
* @throws {ServerError} If `password` is not complex enough
*/
async addUser(username, password) {
throw new ServerError('You must implement this function');
}
/** /**
* Updates the password for the given user * Updates the password for the given user
* *
* @abstract
* @param {string} username - The username of the user * @param {string} username - The username of the user
* @param {string} oldPassword - The current password of the user * @param {string} oldPassword - The current password of the user
* @param {string} newPassword - The new password to set * @param {string} newPassword - The new password to set
* @returns {Promise<void>} * @returns {Promise<void>}
* @throws {ServerError} If not implemented by the subclass * @throws {ServerError} If not implemented by the subclass
* @throws {ServerError} If `newPassword` is not complex enough
*/ */
async updatePassword(username, oldPassword, newPassword) { async updatePassword(username, oldPassword, newPassword) {
throw new ServerError('You must implement this function'); throw new ServerError('You must implement this function');

80
src/lib/database/Lowdb.js

@ -10,7 +10,7 @@ const DatabaseInterface = require('./AbstractDatabase');
const { WG_PATH } = require('../../config'); const { WG_PATH } = require('../../config');
/** * /**
* *
* @typedef {Object} Role * @typedef {Object} Role
* @property {string} ADMIN - Represents an ADMIN user * @property {string} ADMIN - Represents an ADMIN user
@ -25,14 +25,14 @@ const { WG_PATH } = require('../../config');
* @property {Array<User>} users * @property {Array<User>} users
*/ */
/** * /**
* @type {Role} * @type {Role}
*/ */
const Role = { const Role = {
ADMIN: 'admin', ADMIN: 'admin',
}; };
/** * /**
* @type {Model} * @type {Model}
*/ */
const Model = {}; const Model = {};
@ -47,7 +47,7 @@ module.exports = class Lowdb extends DatabaseInterface {
async initDb() { async initDb() {
debug('Loading database...'); debug('Loading database...');
// lowdb is esm module // lowdb is an esm module
// eslint-disable-next-line node/no-unsupported-features/es-syntax, import/no-unresolved, node/no-missing-import // eslint-disable-next-line node/no-unsupported-features/es-syntax, import/no-unresolved, node/no-missing-import
const { JSONFilePreset } = await import('lowdb/node'); const { JSONFilePreset } = await import('lowdb/node');
this.db = await JSONFilePreset(this.dbPath, Model); this.db = await JSONFilePreset(this.dbPath, Model);
@ -55,14 +55,6 @@ module.exports = class Lowdb extends DatabaseInterface {
if (!this.db.data.users) { if (!this.db.data.users) {
this.db.data = { users: [] }; this.db.data = { users: [] };
const defaultPassword = process.env.PASSWORD_HASH || await bcrypt.hash(process.env.PASSWORD || 'admin', 12);
const adminUser = {
id: uuidv4(),
username: 'admin',
password: defaultPassword,
roles: [Role.ADMIN],
};
this.db.data.users.push(adminUser);
await this.db.write(); await this.db.write();
debug(`Created new database at ${this.dbPath}`); debug(`Created new database at ${this.dbPath}`);
} }
@ -80,17 +72,81 @@ module.exports = class Lowdb extends DatabaseInterface {
async updatePassword(username, oldPassword, newPassword) { async updatePassword(username, oldPassword, newPassword) {
debug('Update password'); debug('Update password');
if (!super.isPasswordComplex(newPassword)) {
throw new ServerError('Password does not meet complexity requirements : 8 characters minimum, uppercase, lowercase, number and special char');
}
const user = this.db.data.users.find((u) => u.username === username); const user = this.db.data.users.find((u) => u.username === username);
if (!user) { if (!user) {
throw new ServerError('User not found'); throw new ServerError('User not found');
} }
const isMatch = await bcrypt.compare(oldPassword, user.password); const isMatch = await bcrypt.compare(oldPassword, user.password);
if (!isMatch) { if (!isMatch) {
throw new ServerError('Old password is incorrect'); throw new ServerError('Old password is incorrect');
} }
// lowdb is atomic // lowdb is atomic
user.password = await bcrypt.hash(newPassword, 12); user.password = await bcrypt.hash(newPassword, 12);
await this.db.write(); await this.db.write();
} }
async addUser(username, password) {
debug('Add user');
if (!super.isPasswordComplex(password)) {
throw new ServerError('Password does not meet complexity requirements, minimum 8 characters, uppercase, lowercase, number and special char');
}
const user_ = this.db.data.users.find((u) => u.username === username);
if (user_) {
throw new ServerError('User does already exist');
}
const userPassword = await bcrypt.hash(password, 12);
const user = {
id: uuidv4(),
username,
password: userPassword,
roles: [],
};
this.db.data.users.push(user);
await this.db.write();
}
async addAdminUser(username, password) {
debug('Add admin user');
if (!this.firstSetupAuth()) {
// for the time being, when the first setup, this method is auth to be called, otherwise throw
throw new ServerError('Could not add an admin user', 405);
}
if (!super.isPasswordComplex(password)) {
throw new ServerError('Password does not meet complexity requirements, minimum 8 characters, uppercase, lowercase, number and special char', 403);
}
const user_ = this.db.data.users.find((u) => u.username === username);
if (user_) {
throw new ServerError('User does already exist', 403);
}
const userPassword = await bcrypt.hash(password, 12);
const user = {
id: uuidv4(),
username,
password: userPassword,
roles: [Role.ADMIN],
};
this.db.data.users.push(user);
await this.db.write();
}
firstSetupAuth() {
return this.db.data.users.length === 0;
}
}; };

85
src/templates/Setup.vue

@ -0,0 +1,85 @@
<!-- vue-cli-service build --dest src/www/js/vendor --no-module --formats umd-min --target lib --name VueSetup --filename vue-setup --no-clean src/templates/Setup.vue -->
<script>
export default {
name: 'Setup',
data() {
return {
username: '',
newPassword: '',
confirmNewPassword: '',
};
},
methods: {
async addAdminUser(e) {
e.preventDefault();
if (this.newPassword !== this.confirmNewPassword) {
alert('Password Mismatch');
return;
}
this.api.addAdminUser({
username: this.username,
password: this.newPassword
}).then((_result) => {
this.username = '';
this.newPassword = '';
this.confirmNewPassword = '';
alert('User created');
window.location.reload();
}).catch((err) => {
alert(err.message || err.toString());
});
},
},
mounted() {
this.api = new API();
},
};
</script>
<template>
<div>
<h1 class="text-4xl font-medium my-16 text-gray-700 dark:text-neutral-200 text-center">
<span class="align-middle">{{ $t("setup") }}</span>
</h1>
<form @submit="addAdminUser"
class="shadow rounded-md bg-white dark:bg-neutral-700 mx-auto w-72 p-5 overflow-hidden mt-10">
<div class="h-20 w-20 mb-10 mt-5 mx-auto rounded-full bg-red-800 dark:bg-red-800 relative overflow-hidden">
<svg class="w-10 h-10 m-5 text-white dark:text-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" />
</svg>
</div>
<div class="mb-4">
<label for="username" class="block mb-2 text-gray-500 dark:text-white">{{ $t('setupUsername') }}</label>
<input id="username" name="username" :placeholder="$t('setupUsername')" v-model="username" required
autocomplete="username"
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none" />
</div>
<div class="mb-4">
<label for="newPassword" class="block mb-2 text-gray-500 dark:text-white">{{ $t('newPassword') }}</label>
<input id="newPassword" type="password" name="newPassword" :placeholder="$t('newPassword')"
v-model="newPassword" required autocomplete="new-password"
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none" />
</div>
<div class="mb-4">
<label for="confirmNewPassword" class="block mb-2 text-gray-500 dark:text-white">{{ $t('confirmNewPassword')
}}</label>
<input id="confirmNewPassword" type="password" name="confirmNewPassword" :placeholder="$t('confirmNewPassword')"
v-model="confirmNewPassword" required autocomplete="new-password"
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none" />
</div>
<button type="submit" :disabled="!newPassword || !confirmNewPassword || newPassword != confirmNewPassword"
:class="!newPassword || !confirmNewPassword || newPassword != confirmNewPassword ? 'bg-gray-200 dark:bg-neutral-800 w-full rounded shadow py-2 text-sm text-white dark:text-white cursor-not-allowed' : 'bg-red-800 dark:bg-red-800 w-full rounded shadow py-2 text-sm text-white dark:text-white hover:bg-red-700 dark:hover:bg-red-700 transition cursor-pointer'">
{{ $t("setupBtnCU") }}
</button>
<small class="text-yellow-600">{{ $t("setupRequiredPatternPassword") }}</small>
</form>
</div>
</template>

25
src/www/css/app.css

@ -714,6 +714,10 @@ video {
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
} }
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-4 { .mb-4 {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
@ -883,6 +887,10 @@ video {
width: 16rem; width: 16rem;
} }
.w-72 {
width: 18rem;
}
.w-8 { .w-8 {
width: 2rem; width: 2rem;
} }
@ -972,10 +980,6 @@ video {
flex-wrap: wrap; flex-wrap: wrap;
} }
.items-end {
align-items: flex-end;
}
.items-center { .items-center {
align-items: center; align-items: center;
} }
@ -1323,6 +1327,14 @@ video {
font-weight: 600; font-weight: 600;
} }
.uppercase {
text-transform: uppercase;
}
.lowercase {
text-transform: lowercase;
}
.leading-6 { .leading-6 {
line-height: 1.5rem; line-height: 1.5rem;
} }
@ -1387,6 +1399,11 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity)); color: rgb(255 255 255 / var(--tw-text-opacity));
} }
.text-yellow-600 {
--tw-text-opacity: 1;
color: rgb(202 138 4 / var(--tw-text-opacity));
}
.opacity-0 { .opacity-0 {
opacity: 0; opacity: 0;
} }

1103
src/www/index.html

File diff suppressed because it is too large

23
src/www/js/api.js

@ -5,8 +5,10 @@
class API { class API {
async call({ method, path, body }) { async call({
const res = await fetch(`./api${path}`, { method, path, body, fullPath,
}) {
const res = await fetch(fullPath || `./api${path}`, {
method, method,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -146,10 +148,25 @@ class API {
}); });
} }
async checkFirstSetup() {
return this.call({
method: 'get',
fullPath: '/setup',
});
}
async addAdminUser({ username, password }) {
return this.call({
method: 'post',
fullPath: '/setup/user',
body: { username, password },
});
}
async updatePassword({ username, oldPassword, newPassword }) { async updatePassword({ username, oldPassword, newPassword }) {
return this.call({ return this.call({
method: 'put', method: 'put',
path: '/setting/password', path: '/setup/user',
body: { username, oldPassword, newPassword }, body: { username, oldPassword, newPassword },
}); });
} }

8
src/www/js/app.js

@ -47,9 +47,11 @@ new Vue({
components: { components: {
apexchart: VueApexCharts, apexchart: VueApexCharts,
settings: VueSettings, settings: VueSettings,
setup: VueSetup,
}, },
i18n, i18n,
data: { data: {
firstSetup: false,
authenticated: null, authenticated: null,
authenticating: false, authenticating: false,
// temp username 'admin' // temp username 'admin'
@ -355,6 +357,12 @@ new Vue({
this.setTheme(this.uiTheme); this.setTheme(this.uiTheme);
this.api = new API(); this.api = new API();
this.api.checkFirstSetup()
.then((response) => {
this.firstSetup = response.success;
}).catch((err) => {
alert(err.message || err.toString());
});
this.api.getSession() this.api.getSession()
.then((session) => { .then((session) => {
this.authenticated = session.authenticated; this.authenticated = session.authenticated;

4
src/www/js/i18n.js

@ -34,6 +34,10 @@ const messages = { // eslint-disable-line no-unused-vars
backup: 'Backup', backup: 'Backup',
titleRestoreConfig: 'Restore your configuration', titleRestoreConfig: 'Restore your configuration',
titleBackupConfig: 'Backup your configuration', titleBackupConfig: 'Backup your configuration',
setup: 'Setup',
setupUsername: 'Username',
setupBtnCU: 'Create User',
setupRequiredPatternPassword: '12 characters minimum, 1 uppercase, 1 lowercase, 1 number and 1 special char',
navHome: 'Home', navHome: 'Home',
navSettings: 'Settings', navSettings: 'Settings',
settings: 'Settings', settings: 'Settings',

2
src/www/js/vendor/vue-setup.umd.min.js

File diff suppressed because one or more lines are too long

1
src/www/js/vendor/vue-setup.umd.min.js.map

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save