diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..a6cc5e5a --- /dev/null +++ b/.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 +} \ No newline at end of file diff --git a/src/lib/Server.js b/src/lib/Server.js index 3de37c73..d777bb7e 100644 --- a/src/lib/Server.js +++ b/src/lib/Server.js @@ -129,7 +129,7 @@ module.exports = class Server { // WireGuard app.use( fromNodeMiddleware(async (req, res, next) => { - if (!requiresPassword || !req.url.startsWith('/api/')) { + if (!req.url.startsWith('/api/')) { return next(); } @@ -273,12 +273,33 @@ module.exports = class Server { return { success: true }; })); - // setting - const routerSetting = createRouter(); - app.use(routerSetting); + // Setup + const routerSetup = createRouter(); + app.use(routerSetup); - routerSetting - .put('/api/setting/password', defineEventHandler(async (event) => { + routerSetup + .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); await Database.updatePassword(username, oldPassword, newPassword); return { success: true }; diff --git a/src/lib/database/AbstractDatabase.js b/src/lib/database/AbstractDatabase.js index aaa1bb11..32f6bcf0 100644 --- a/src/lib/database/AbstractDatabase.js +++ b/src/lib/database/AbstractDatabase.js @@ -2,6 +2,12 @@ const ServerError = require('../ServerError'); +/** + * All database implementations must extend this class and implement the following methods + * + * @abstract + * + */ module.exports = class DatabaseInterface { 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 * + * @abstract * @returns {Promise} * @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 * + * @abstract * @param {string} username - The username of the user * @param {string} password - The password to compare - * @returns {Promise} - Return `true` if the password matches, otherwise `false` + * @returns {Promise} Return `true` if the password matches, otherwise `false` * @throws {ServerError} If not implemented by the subclass */ async comparePassword(username, password) { 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} + * @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 * + * @abstract * @param {string} username - The username of the user * @param {string} oldPassword - The current password of the user * @param {string} newPassword - The new password to set * @returns {Promise} * @throws {ServerError} If not implemented by the subclass + * @throws {ServerError} If `newPassword` is not complex enough */ async updatePassword(username, oldPassword, newPassword) { throw new ServerError('You must implement this function'); diff --git a/src/lib/database/Lowdb.js b/src/lib/database/Lowdb.js index eeb65ec5..103f981e 100644 --- a/src/lib/database/Lowdb.js +++ b/src/lib/database/Lowdb.js @@ -10,7 +10,7 @@ const DatabaseInterface = require('./AbstractDatabase'); const { WG_PATH } = require('../../config'); -/** * +/** * * @typedef {Object} Role * @property {string} ADMIN - Represents an ADMIN user @@ -25,14 +25,14 @@ const { WG_PATH } = require('../../config'); * @property {Array} users */ -/** * +/** * @type {Role} */ const Role = { ADMIN: 'admin', }; -/** * +/** * @type {Model} */ const Model = {}; @@ -47,7 +47,7 @@ module.exports = class Lowdb extends DatabaseInterface { async initDb() { 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 const { JSONFilePreset } = await import('lowdb/node'); this.db = await JSONFilePreset(this.dbPath, Model); @@ -55,14 +55,6 @@ module.exports = class Lowdb extends DatabaseInterface { if (!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(); debug(`Created new database at ${this.dbPath}`); } @@ -80,17 +72,81 @@ module.exports = class Lowdb extends DatabaseInterface { async updatePassword(username, oldPassword, newPassword) { 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); if (!user) { throw new ServerError('User not found'); } + const isMatch = await bcrypt.compare(oldPassword, user.password); if (!isMatch) { throw new ServerError('Old password is incorrect'); } + // lowdb is atomic user.password = await bcrypt.hash(newPassword, 12); 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; + } + }; diff --git a/src/templates/Setup.vue b/src/templates/Setup.vue new file mode 100644 index 00000000..e08928b0 --- /dev/null +++ b/src/templates/Setup.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/src/www/css/app.css b/src/www/css/app.css index fb9414bc..487e782a 100644 --- a/src/www/css/app.css +++ b/src/www/css/app.css @@ -714,6 +714,10 @@ video { margin-bottom: 2.5rem; } +.mb-2 { + margin-bottom: 0.5rem; +} + .mb-4 { margin-bottom: 1rem; } @@ -883,6 +887,10 @@ video { width: 16rem; } +.w-72 { + width: 18rem; +} + .w-8 { width: 2rem; } @@ -972,10 +980,6 @@ video { flex-wrap: wrap; } -.items-end { - align-items: flex-end; -} - .items-center { align-items: center; } @@ -1323,6 +1327,14 @@ video { font-weight: 600; } +.uppercase { + text-transform: uppercase; +} + +.lowercase { + text-transform: lowercase; +} + .leading-6 { line-height: 1.5rem; } @@ -1387,6 +1399,11 @@ video { 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; } diff --git a/src/www/index.html b/src/www/index.html index 40e2dde2..93d03fd9 100644 --- a/src/www/index.html +++ b/src/www/index.html @@ -24,232 +24,211 @@
-
-
-

- WireGuard -

-
- - - - - - {{$t("logout")}} - - - - -
-
-
-
-
-
-

{{$t("updateAvailable")}}

-

{{latestRelease.changelog}}

+ d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25"/> + + + + + + {{$t("logout")}} + + + +
- - - {{$t("update")}} → -
-
- -
- - +
- -
-
-
-

{{$t("clients")}}

+
+ + + + +
+
+
+

{{$t("clients")}}

+
+
+ + + + + + + + + {{$t("backup")}} + + + +
-
-
- -
+
+ +
- -
- - -
-
- - -
+ +
+ + +
+
+ + +
-
-
+
+
- -
- - - - + +
+ + + + -
+ v-if="client.latestHandshakeAt && ((new Date() - new Date(client.latestHandshakeAt) < 1000 * 60 * 10))"> +
+
+
-
-
- -
- - -
-
- - - - {{client.name}} - - - - - - - -
- -
- + +
+ + +
+
+ - - {{client.address}} + + {{client.name}} - - - - - · - - - - {{client.transferTxCurrent | bytes}}/s - - - - - · - - - - {{client.transferRxCurrent | bytes}}/s - - - - {{!uiTrafficStats ? " · " : ""}}{{new Date(client.latestHandshakeAt) | timeago}} - -
-
- - -
- - -
- - - - -
- {{client.transferTxCurrent | - bytes}}/s - -
{{bytes(client.transferTx)}} -
-
+
+ +
+ + + + {{client.address}} + + + + + + + + + + + · + + + + {{client.transferTxCurrent | bytes}}/s + + + + · + + + + {{client.transferRxCurrent | bytes}}/s + + + + {{!uiTrafficStats ? " · " : ""}}{{new Date(client.latestHandshakeAt) | timeago}} + +
- -
- - - - - -
- {{client.transferRxCurrent | - bytes}}/s - -
{{bytes(client.transferRx)}} -
-
-
+ +
+ + +
+ + + + +
+ {{client.transferTxCurrent | + bytes}}/s + +
{{bytes(client.transferTx)}} +
+
-
-
- -
+
-
-
+ +
+ - -
-
+ + + +
+ {{client.transferRxCurrent | + bytes}}/s + +
{{bytes(client.transferRx)}} +
+ +
+ +
+ +
-
+
+
-
-
+ +
+
+
- - - - - - - - - - +
- +
+
- + + + + + + + + + + + + + + +
+
- -
-
-

- {{$t("noClients")}}

- -

-
-
- - - - - +
+

+ {{$t("noClients")}}

+ +

+
+
+ + + + + +
-
- - -
+ + +
- -
-
-
- - + +
+
+
+ + +
-
- -
-
- - + +
+
+ + - - - -