mirror of https://github.com/wg-easy/wg-easy
Browse Source
- add: in-memory database provider - create a new account (signup) - login with username and password - first setup page to create an account - PASSWORD_HASH was removed from environment and files was updated/removed due to that changepull/1330/head
26 changed files with 376 additions and 258 deletions
@ -1,36 +0,0 @@ |
|||
# wg-password |
|||
|
|||
`wg-password` (wgpw) is a script that generates bcrypt password hashes for use with `wg-easy`, enhancing security by requiring passwords. |
|||
|
|||
## Features |
|||
|
|||
- Generate bcrypt password hashes. |
|||
- Easily integrate with `wg-easy` to enforce password requirements. |
|||
|
|||
## Usage with Docker |
|||
|
|||
To generate a bcrypt password hash using docker, run the following command : |
|||
|
|||
```sh |
|||
docker run ghcr.io/wg-easy/wg-easy wgpw YOUR_PASSWORD |
|||
PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW' // literally YOUR_PASSWORD |
|||
``` |
|||
|
|||
**Important** : make sure to enclose your password in **single quotes** when you run `docker run` command : |
|||
|
|||
```bash |
|||
$ echo $2b$12$coPqCsPtcF <-- not correct |
|||
b2 |
|||
$ echo "$2b$12$coPqCsPtcF" <-- not correct |
|||
b2 |
|||
$ echo '$2b$12$coPqCsPtcF' <-- correct |
|||
$2b$12$coPqCsPtcF |
|||
``` |
|||
|
|||
**Important** : Please note: don't wrap the generated hash password in single quotes when you use `docker-compose.yml`. Instead, replace each `$` symbol with two `$$` symbols. For example: |
|||
|
|||
``` yaml |
|||
- PASSWORD_HASH=$$2y$$10$$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG |
|||
``` |
|||
|
|||
This hash is for the password 'foobar123', obtained using the command `docker run ghcr.io/wg-easy/wg-easy wgpw foobar123` and then inserted an additional `$` before each existing `$` symbal. |
@ -1,5 +0,0 @@ |
|||
import type { InMemory } from '~/adapters/database/inmemory'; |
|||
|
|||
export default (): InMemory => { |
|||
return useNuxtApp().$database; |
|||
}; |
@ -0,0 +1,60 @@ |
|||
<template> |
|||
<main> |
|||
<div> |
|||
<h1>Welcome to your first setup of wg-easy !</h1> |
|||
<p>Please first enter an admin username and a strong password.</p> |
|||
<form @submit="newAccount"> |
|||
<div> |
|||
<label for="username">Username</label> |
|||
<input |
|||
id="username" |
|||
v-model="username" |
|||
type="text" |
|||
name="username" |
|||
autocomplete="username" |
|||
/> |
|||
</div> |
|||
<div> |
|||
<label for="password">New Password</label> |
|||
<input |
|||
id="password" |
|||
v-model="password" |
|||
type="password" |
|||
name="password" |
|||
autocomplete="new-password" |
|||
/> |
|||
</div> |
|||
<div> |
|||
<label for="accept">I accept the condition.</label> |
|||
<input id="accept" type="checkbox" name="accept" /> |
|||
</div> |
|||
<button type="submit">Save</button> |
|||
</form> |
|||
</div> |
|||
</main> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const username = ref<string>(); |
|||
const password = ref<string>(); |
|||
const authStore = useAuthStore(); |
|||
|
|||
async function newAccount(e: Event) { |
|||
e.preventDefault(); |
|||
|
|||
if (!username.value) return; |
|||
if (!password.value) return; |
|||
|
|||
try { |
|||
const res = await authStore.signup(username.value, password.value); |
|||
if (res) { |
|||
navigateTo('/login'); |
|||
} |
|||
} catch (error) { |
|||
if (error instanceof Error) { |
|||
// TODO: replace alert with actual ui error message |
|||
alert(error.message || error.toString()); |
|||
} |
|||
} |
|||
} |
|||
</script> |
@ -1,15 +0,0 @@ |
|||
/** |
|||
* Changing the Database Provider |
|||
* This design allows for easy swapping of different database implementations. |
|||
* |
|||
*/ |
|||
|
|||
import initInMemoryProvider from '~/adapters/database/inmemory'; |
|||
|
|||
export default defineNuxtPlugin(() => { |
|||
return { |
|||
provide: { |
|||
database: initInMemoryProvider(), |
|||
}, |
|||
}; |
|||
}); |
@ -0,0 +1,22 @@ |
|||
import { DatabaseError } from '~/ports/database'; |
|||
|
|||
type Request = { username: string; password: string }; |
|||
|
|||
export default defineEventHandler(async (event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
try { |
|||
// TODO use zod
|
|||
const { username, password } = await readBody<Request>(event); |
|||
await Database.newUserWithPassword(username, password); |
|||
return { success: true }; |
|||
} catch (error) { |
|||
if (error instanceof DatabaseError) { |
|||
throw createError({ |
|||
statusCode: 400, |
|||
statusMessage: error.message, |
|||
}); |
|||
} else { |
|||
throw createError('Something happened !'); |
|||
} |
|||
} |
|||
}); |
@ -1,7 +1,4 @@ |
|||
import useDatabase from '~/composables/useDatabase'; |
|||
|
|||
export default defineEventHandler(async (event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
const db = useDatabase(); |
|||
return db.getLang(); |
|||
return await Database.getLang(); |
|||
}); |
|||
|
@ -0,0 +1,16 @@ |
|||
/* First setup of wg-easy app */ |
|||
export default defineEventHandler(async (event) => { |
|||
const url = getRequestURL(event); |
|||
|
|||
if ( |
|||
url.pathname.startsWith('/setup') || |
|||
url.pathname === '/api/account/new' |
|||
) { |
|||
return; |
|||
} |
|||
|
|||
const users = await Database.getUsers(); |
|||
if (users.length === 0) { |
|||
return sendRedirect(event, '/setup', 302); |
|||
} |
|||
}); |
@ -0,0 +1,16 @@ |
|||
/** |
|||
* Changing the Database Provider |
|||
* This design allows for easy swapping of different database implementations. |
|||
* |
|||
*/ |
|||
|
|||
import InMemory from '~/adapters/database/inmemory'; |
|||
|
|||
const provider = new InMemory(); |
|||
|
|||
provider.connect().catch((err) => { |
|||
console.error(err); |
|||
process.exit(1); |
|||
}); |
|||
|
|||
export default provider; |
@ -1,17 +1,47 @@ |
|||
import bcrypt from 'bcryptjs'; |
|||
|
|||
/** |
|||
* Checks if `password` matches the PASSWORD_HASH. |
|||
* |
|||
* If environment variable is not set, the password is always invalid. |
|||
* Checks if `password` matches the user password. |
|||
* |
|||
* @param {string} password String to test |
|||
* @returns {boolean} true if matching environment, otherwise false |
|||
* @returns {boolean} `true` if matching user password, otherwise `false` |
|||
*/ |
|||
export function isPasswordValid(password: string, hash?: string): boolean { |
|||
if (hash) { |
|||
export function isPasswordValid(password: string, hash: string): boolean { |
|||
return bcrypt.compareSync(password, hash); |
|||
} |
|||
|
|||
/** |
|||
* Checks if a password is strong based on following criteria : |
|||
* |
|||
* - minimum length of 12 characters |
|||
* - contains at least one uppercase letter |
|||
* - contains at least one lowercase letter |
|||
* - contains at least one number |
|||
* - contains at least one special character (e.g., !@#$%^&*(),.?":{}|<>). |
|||
* |
|||
* @param {string} password - The password to validate |
|||
* @returns {boolean} `true` if the password is strong, otherwise `false` |
|||
*/ |
|||
|
|||
export function isPasswordStrong(password: string): boolean { |
|||
if (password.length < 12) { |
|||
return false; |
|||
} |
|||
|
|||
const hasUpperCase = /[A-Z]/.test(password); |
|||
const hasLowerCase = /[a-z]/.test(password); |
|||
const hasNumber = /\d/.test(password); |
|||
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); |
|||
|
|||
return hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar; |
|||
} |
|||
|
|||
/** |
|||
* Hashes a password using the bcrypt algorithm. |
|||
* |
|||
* @param {string} password - The plaintext password to hash |
|||
* @returns {string} The bcrypt hash of the password |
|||
*/ |
|||
export function hashPasswordWithBcrypt(password: string): string { |
|||
return bcrypt.hashSync(password, 12); |
|||
} |
|||
|
@ -1,49 +0,0 @@ |
|||
// Import needed libraries
|
|||
import bcrypt from 'bcryptjs'; |
|||
|
|||
// Function to generate hash
|
|||
const generateHash = async (password) => { |
|||
try { |
|||
const salt = await bcrypt.genSalt(12); |
|||
const hash = await bcrypt.hash(password, salt); |
|||
|
|||
console.log(`PASSWORD_HASH='${hash}'`); |
|||
} catch (error) { |
|||
throw new Error(`Failed to generate hash : ${error}`); |
|||
} |
|||
}; |
|||
|
|||
// Function to compare password with hash
|
|||
const comparePassword = async (password, hash) => { |
|||
try { |
|||
const match = await bcrypt.compare(password, hash); |
|||
if (match) { |
|||
console.log('Password matches the hash !'); |
|||
} else { |
|||
console.log('Password does not match the hash.'); |
|||
} |
|||
} catch (error) { |
|||
throw new Error(`Failed to compare password and hash : ${error}`); |
|||
} |
|||
}; |
|||
|
|||
(async () => { |
|||
try { |
|||
// Retrieve command line arguments
|
|||
const args = process.argv.slice(2); // Ignore the first two arguments
|
|||
if (args.length > 2) { |
|||
throw new Error('Usage : wgpw YOUR_PASSWORD [HASH]'); |
|||
} |
|||
|
|||
const [password, hash] = args; |
|||
if (password && hash) { |
|||
await comparePassword(password, hash); |
|||
} else if (password) { |
|||
await generateHash(password); |
|||
} |
|||
} catch (error) { |
|||
console.error(error); |
|||
|
|||
process.exit(1); |
|||
} |
|||
})(); |
@ -1,5 +0,0 @@ |
|||
#!/bin/sh |
|||
# This script is intended to be run only inside a docker container, not on the development host machine |
|||
set -e |
|||
# proxy command |
|||
node /app/wgpw.mjs "$@" |
Loading…
Reference in new issue