Browse Source

fix: review done

- remove: REQUIRES_PASSWORD & RELEASE environment variables
pull/1330/head
tetuaoro 2 years ago
parent
commit
d883e8a63b
  1. 55
      src/adapters/database/inmemory.ts
  2. 5
      src/i18n.config.ts
  3. 2
      src/package.json
  4. 6
      src/pages/login.vue
  5. 7
      src/pages/setup.vue
  6. 17
      src/pnpm-lock.yaml
  7. 27
      src/ports/database.ts
  8. 10
      src/ports/system/model.ts
  9. 33
      src/ports/types.ts
  10. 18
      src/ports/user/interface.ts
  11. 27
      src/ports/user/model.ts
  12. 8
      src/server/api/account/new.post.ts
  13. 2
      src/server/api/lang.get.ts
  14. 10
      src/server/api/release.get.ts
  15. 6
      src/server/api/session.get.ts
  16. 19
      src/server/api/session.post.ts
  17. 4
      src/server/middleware/auth.ts
  18. 1
      src/server/middleware/session.ts
  19. 4
      src/server/utils/config.ts
  20. 8
      src/server/utils/password.ts
  21. 10
      src/server/utils/types.ts

55
src/adapters/database/inmemory.ts

@ -3,18 +3,14 @@ import packageJson from '@/package.json';
import DatabaseProvider, { DatabaseError } from '~/ports/database'; import DatabaseProvider, { DatabaseError } from '~/ports/database';
import { ChartType, Lang } from '~/ports/types'; import { ChartType, Lang } from '~/ports/types';
import { ROLE } from '~/ports/user/model';
import type { SessionConfig } from 'h3'; import type { SessionConfig } from 'h3';
import type { ID } from '~/ports/types';
import type { System } from '~/ports/system/model'; import type { System } from '~/ports/system/model';
import type { User } from '~/ports/user/model'; import type { User } from '~/ports/user/model';
import type { Identity, String } from '~/ports/types'; import { hashPassword, isPasswordStrong } from '~/server/utils/password';
import {
hashPasswordWithBcrypt,
isPasswordStrong,
} from '~/server/utils/password';
const INMDP_DEBUG = debug('InMemoryDP'); const DEBUG = debug('InMemoryDB');
// Represent in-memory data structure // Represent in-memory data structure
type InMemoryData = { type InMemoryData = {
@ -27,7 +23,7 @@ export default class InMemory extends DatabaseProvider {
protected data: InMemoryData = { users: [] }; protected data: InMemoryData = { users: [] };
async connect() { async connect() {
INMDP_DEBUG('Connection...'); DEBUG('Connection...');
const system: System = { const system: System = {
release: packageJson.release.version, release: packageJson.release.version,
interface: { interface: {
@ -74,7 +70,7 @@ export default class InMemory extends DatabaseProvider {
}; };
this.data.system = system; this.data.system = system;
INMDP_DEBUG('Connection done'); DEBUG('Connection done');
} }
async disconnect() { async disconnect() {
@ -82,15 +78,10 @@ export default class InMemory extends DatabaseProvider {
} }
async getSystem() { async getSystem() {
INMDP_DEBUG('Get System'); DEBUG('Get System');
return this.data.system; return this.data.system;
} }
async saveSystem(system: System) {
INMDP_DEBUG('Save System');
this.data.system = system;
}
async getLang() { async getLang() {
return this.data.system?.lang || Lang.EN; return this.data.system?.lang || Lang.EN;
} }
@ -99,18 +90,15 @@ export default class InMemory extends DatabaseProvider {
return this.data.users; return this.data.users;
} }
async getUser(id: Identity<User>) { async getUser(id: ID) {
INMDP_DEBUG('Get User'); DEBUG('Get User');
if (typeof id === 'string' || typeof id === 'number') { return this.data.users.find((user) => user.id === id);
return this.data.users.find((user) => user.id === id);
}
return this.data.users.find((user) => user.id === id.id);
} }
async newUserWithPassword(username: String, password: String) { async newUserWithPassword(username: string, password: string) {
INMDP_DEBUG('New User'); DEBUG('New User');
if (username.length < 8) { if (username.length < 8) {
throw new DatabaseError(DatabaseError.ERROR_USERNAME_LEN); throw new DatabaseError(DatabaseError.ERROR_USERNAME_REQ);
} }
if (!isPasswordStrong(password)) { if (!isPasswordStrong(password)) {
@ -125,13 +113,13 @@ export default class InMemory extends DatabaseProvider {
} }
const now = new Date(); const now = new Date();
const isUserEmpty = this.data.users.length == 0; const isUserEmpty = this.data.users.length === 0;
const newUser: User = { const newUser: User = {
id: `${this.data.users.length + 1}`, id: `${this.data.users.length + 1}`,
password: hashPasswordWithBcrypt(password), password: hashPassword(password),
username, username,
role: isUserEmpty ? ROLE.ADMIN : ROLE.CLIENT, role: isUserEmpty ? 'ADMIN' : 'CLIENT',
enabled: true, enabled: true,
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
@ -140,18 +128,17 @@ export default class InMemory extends DatabaseProvider {
this.data.users.push(newUser); this.data.users.push(newUser);
} }
async saveUser(user: User) { async updateUser(user: User) {
let _user = await this.getUser(user); let _user = await this.getUser(user.id);
if (_user) { if (_user) {
INMDP_DEBUG('Update User'); DEBUG('Update User');
_user = user; _user = user;
} }
} }
async deleteUser(id: Identity<User>) { async deleteUser(id: ID) {
INMDP_DEBUG('Delete User'); DEBUG('Delete User');
const _id = typeof id === 'string' || typeof id === 'number' ? id : id.id; const idx = this.data.users.findIndex((user) => user.id === id);
const idx = this.data.users.findIndex((user) => user.id == _id);
if (idx !== -1) { if (idx !== -1) {
this.data.users.splice(idx, 1); this.data.users.splice(idx, 1);
} }

5
src/i18n.config.ts

@ -46,6 +46,11 @@ export default defineI18nConfig(() => ({
ExpireDate: 'Expire Date', ExpireDate: 'Expire Date',
Permanent: 'Permanent', Permanent: 'Permanent',
OneTimeLink: 'Generate short one time link', OneTimeLink: 'Generate short one time link',
errorDatabaseConn: 'Failed to connect to the database.',
errorPasswordReq:
'Password does not meet the strength requirements. It must be at least 12 characters long, with at least one uppercase letter, one lowercase letter, one number, and one special character.',
errorUsernameReq: 'Username must be longer than 8 characters.',
errorUserExist: 'User already exists.',
}, },
ua: { ua: {
name: 'Ім`я', name: 'Ім`я',

2
src/package.json

@ -35,6 +35,7 @@
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.10",
"timeago.js": "^4.0.2", "timeago.js": "^4.0.2",
"uuid": "^10.0.0",
"vue": "latest", "vue": "latest",
"vue3-apexcharts": "^1.5.3", "vue3-apexcharts": "^1.5.3",
"zod": "^3.23.8" "zod": "^3.23.8"
@ -44,6 +45,7 @@
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/debug": "^4.1.12", "@types/debug": "^4.1.12",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/uuid": "^10.0.0",
"eslint": "^9.8.0", "eslint": "^9.8.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"prettier": "^3.3.3", "prettier": "^3.3.3",

6
src/pages/login.vue

@ -86,7 +86,7 @@
<script setup lang="ts"> <script setup lang="ts">
const authenticating = ref(false); const authenticating = ref(false);
const remember = ref(false); const remember = ref(false);
const username = ref<string>(); const username = ref<null | string>(null);
const password = ref<null | string>(null); const password = ref<null | string>(null);
const authStore = useAuthStore(); const authStore = useAuthStore();
const globalStore = useGlobalStore(); const globalStore = useGlobalStore();
@ -94,9 +94,7 @@ const globalStore = useGlobalStore();
async function login(e: Event) { async function login(e: Event) {
e.preventDefault(); e.preventDefault();
if (!username.value) return; if (!username.value || !password.value || authenticating.value) return;
if (!password.value) return;
if (authenticating.value) return;
authenticating.value = true; authenticating.value = true;
try { try {

7
src/pages/setup.vue

@ -35,15 +35,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const username = ref<string>(); const username = ref<null | string>(null);
const password = ref<string>(); const password = ref<null | string>(null);
const authStore = useAuthStore(); const authStore = useAuthStore();
async function newAccount(e: Event) { async function newAccount(e: Event) {
e.preventDefault(); e.preventDefault();
if (!username.value) return; if (!username.value || !password.value) return;
if (!password.value) return;
try { try {
const res = await authStore.signup(username.value, password.value); const res = await authStore.signup(username.value, password.value);

17
src/pnpm-lock.yaml

@ -56,6 +56,9 @@ importers:
timeago.js: timeago.js:
specifier: ^4.0.2 specifier: ^4.0.2
version: 4.0.2 version: 4.0.2
uuid:
specifier: ^10.0.0
version: 10.0.0
vue: vue:
specifier: latest specifier: latest
version: 3.4.37([email protected]) version: 3.4.37([email protected])
@ -78,6 +81,9 @@ importers:
'@types/qrcode': '@types/qrcode':
specifier: ^1.5.5 specifier: ^1.5.5
version: 1.5.5 version: 1.5.5
'@types/uuid':
specifier: ^10.0.0
version: 10.0.0
eslint: eslint:
specifier: ^9.8.0 specifier: ^9.8.0
version: 9.8.0 version: 9.8.0
@ -1254,6 +1260,9 @@ packages:
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
'@types/[email protected]':
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
'@typescript-eslint/[email protected]': '@typescript-eslint/[email protected]':
resolution: {integrity: sha512-STIZdwEQRXAHvNUS6ILDf5z3u95Gc8jzywunxSNqX00OooIemaaNIA0vEgynJlycL5AjabYLLrIyHd4iazyvtg==} resolution: {integrity: sha512-STIZdwEQRXAHvNUS6ILDf5z3u95Gc8jzywunxSNqX00OooIemaaNIA0vEgynJlycL5AjabYLLrIyHd4iazyvtg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -4203,6 +4212,10 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
[email protected]:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true
[email protected]: [email protected]:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
@ -5727,6 +5740,8 @@ snapshots:
'@types/[email protected]': {} '@types/[email protected]': {}
'@types/[email protected]': {}
'@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected])': '@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected])':
dependencies: dependencies:
'@eslint-community/regexpp': 4.11.0 '@eslint-community/regexpp': 4.11.0
@ -9063,6 +9078,8 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: [email protected]:
dependencies: dependencies:
spdx-correct: 3.2.0 spdx-correct: 3.2.0

27
src/ports/database.ts

@ -1,6 +1,6 @@
import type SystemRepository from './system/interface'; import type SystemRepository from './system/interface';
import type UserRepository from './user/interface'; import type UserRepository from './user/interface';
import type { Identity, Undefined, Lang, String } from './types'; import type { Undefined, Lang, ID } from './types';
import type { User } from './user/model'; import type { User } from './user/model';
import type { System } from './system/model'; import type { System } from './system/model';
@ -28,25 +28,24 @@ export default abstract class DatabaseProvider
abstract getLang(): Promise<Lang>; abstract getLang(): Promise<Lang>;
abstract getUsers(): Promise<Array<User>>; abstract getUsers(): Promise<Array<User>>;
abstract getUser(_id: Identity<User>): Promise<User | Undefined>; abstract getUser(id: ID): Promise<User | Undefined>;
abstract newUserWithPassword( abstract newUserWithPassword(
_username: String, username: string,
_password: String password: string
): Promise<void>; ): Promise<void>;
abstract saveUser(_user: User): Promise<void>; abstract updateUser(_user: User): Promise<void>;
abstract deleteUser(_id: Identity<User>): Promise<void>; abstract deleteUser(id: ID): Promise<void>;
} }
export class DatabaseError extends Error { export class DatabaseError extends Error {
static readonly ERROR_PASSWORD_REQ = static readonly ERROR_PASSWORD_REQ = 'errorPasswordReq';
'Password does not meet the strength requirements. It must be at least 12 characters long, with at least one uppercase letter, one lowercase letter, one number, and one special character.'; static readonly ERROR_USER_EXIST = 'errorUserExist';
static readonly ERROR_USER_EXIST = 'User already exists.'; static readonly ERROR_DATABASE_CONNECTION = 'errorDatabaseConn';
static readonly ERROR_DATABASE_CONNECTION = static readonly ERROR_USERNAME_REQ = 'errorUsernameReq';
'Failed to connect to the database.';
static readonly ERROR_USERNAME_LEN =
'Username must be longer than 8 characters.';
constructor(message: string) { constructor(messageKey: string) {
const { t } = useI18n();
const message = t(messageKey);
super(message); super(message);
this.name = 'DatabaseError'; this.name = 'DatabaseError';
} }

10
src/ports/system/model.ts

@ -6,8 +6,6 @@ import type {
Port, Port,
Prometheus, Prometheus,
SessionTimeOut, SessionTimeOut,
String,
Boolean,
TrafficStats, TrafficStats,
Version, Version,
WGConfig, WGConfig,
@ -22,15 +20,15 @@ export type System = {
release: Version; release: Version;
port: number; port: number;
webuiHost: String; webuiHost: string;
// maxAge // maxAge
sessionTimeout: SessionTimeOut; sessionTimeout: SessionTimeOut;
lang: Lang; lang: Lang;
userConfig: WGConfig; userConfig: WGConfig;
wgPath: String; wgPath: string;
wgDevice: String; wgDevice: string;
wgHost: Address; wgHost: Address;
wgPort: Port; wgPort: Port;
wgConfigPort: Port; wgConfigPort: Port;
@ -38,7 +36,7 @@ export type System = {
iptables: IpTables; iptables: IpTables;
trafficStats: TrafficStats; trafficStats: TrafficStats;
wgEnableExpiresTime: Boolean; wgEnableExpiresTime: boolean;
prometheus: Prometheus; prometheus: Prometheus;
sessionConfig: SessionConfig; sessionConfig: SessionConfig;
}; };

33
src/ports/types.ts

@ -5,17 +5,14 @@ export enum Lang {
/* french */ /* french */
FR = 'fr', FR = 'fr',
} }
export type String = string; export type ID = string;
export type Number = number; export type Version = string;
export type ID = String | Number; export type SessionTimeOut = number;
export type Boolean = boolean; export type Port = number;
export type Version = String; export type Address = string;
export type SessionTimeOut = Number; export type HashPassword = string;
export type Port = Number; export type Command = string;
export type Address = String; export type Key = string;
export type HashPassword = String;
export type Command = String;
export type Key = String;
export type IpTables = { export type IpTables = {
wgPreUp: Command; wgPreUp: Command;
wgPostUp: Command; wgPostUp: Command;
@ -28,8 +25,8 @@ export type WGInterface = {
address: Address; address: Address;
}; };
export type WGConfig = { export type WGConfig = {
mtu: Number; mtu: number;
persistentKeepalive: Number; persistentKeepalive: number;
rangeAddress: Address; rangeAddress: Address;
defaultDns: Array<Address>; defaultDns: Array<Address>;
allowedIps: Array<Address>; allowedIps: Array<Address>;
@ -41,16 +38,10 @@ export enum ChartType {
Bar = 3, Bar = 3,
} }
export type TrafficStats = { export type TrafficStats = {
enabled: Boolean; enabled: boolean;
type: ChartType; type: ChartType;
}; };
export type Prometheus = { export type Prometheus = {
enabled: Boolean; enabled: boolean;
password?: HashPassword | Undefined; password?: HashPassword | Undefined;
}; };
/**
* `id` of T or T.
*
* @template T - The specific type that can be used in place of id String.
*/
export type Identity<T> = ID | T;

18
src/ports/user/interface.ts

@ -1,4 +1,4 @@
import type { Identity, String, Undefined } from '../types'; import type { ID, Undefined } from '../types';
import type { User } from './model'; import type { User } from './model';
/** /**
@ -14,28 +14,26 @@ export default interface UserRepository {
/** /**
* Retrieves a user by their ID or User object from the database. * Retrieves a user by their ID or User object from the database.
* @param {Identity<User>} id - The ID of the user or a User object. * @param {ID} id - The ID of the user or a User object.
* @returns {Promise<User | Undefined>} A promise that resolves to the user data * @returns {Promise<User | Undefined>} A promise that resolves to the user data
* if found, or `undefined` if the user is not available. * if found, or `undefined` if the user is not available.
*/ */
getUser(id: Identity<User>): Promise<User | Undefined>; getUser(id: ID): Promise<User | Undefined>;
newUserWithPassword(username: String, password: String): Promise<void>; newUserWithPassword(username: string, password: string): Promise<void>;
/** /**
* Creates or updates a user in the database. * Updates a user in the database.
* @param {User} user - The user to be saved. * @param {User} user - The user to be saved.
* *
* **Note:** If the user already exists, this method will update their details.
* If the user does not exist, it will create a new user entry.
* @returns {Promise<void>} A promise that resolves when the operation is complete. * @returns {Promise<void>} A promise that resolves when the operation is complete.
*/ */
saveUser(user: User): Promise<void>; updateUser(user: User): Promise<void>;
/** /**
* Deletes a user from the database. * Deletes a user from the database.
* @param {Identity<User>} id - The ID of the user or a User object to be deleted. * @param {ID} id - The ID of the user or a User object to be deleted.
* @returns {Promise<void>} A promise that resolves when the user has been deleted. * @returns {Promise<void>} A promise that resolves when the user has been deleted.
*/ */
deleteUser(id: Identity<User>): Promise<void>; deleteUser(id: ID): Promise<void>;
} }

27
src/ports/user/model.ts

@ -1,13 +1,14 @@
import type { Address, ID, Key, HashPassword, String, Boolean } from '../types'; import type { Address, ID, Key, HashPassword } from '../types';
export enum ROLE { /**
/* Full permissions to any resources (app, database...) */ * Represents user roles within the application, each with specific permissions :
ADMIN = 'ADMIN', *
/* Grants write and read permissions on their own resources and `CLIENT` resources without `ADMIN` permissions */ * - `ADMIN`: Full permissions to all resources, including the app, database, etc
EDITOR = 'EDITOR', * - `EDITOR`: Granted write and read permissions on their own resources as well as
/* Grants write and read permissions on their own resources */ * `CLIENT` resources, but without `ADMIN` privileges
CLIENT = 'CLIENT', * - `CLIENT`: Granted write and read permissions only on their own resources.
} */
export type ROLE = 'ADMIN' | 'EDITOR' | 'CLIENT';
/** /**
* Representing a user data structure. * Representing a user data structure.
@ -15,14 +16,14 @@ export enum ROLE {
export type User = { export type User = {
id: ID; id: ID;
role: ROLE; role: ROLE;
username: String; username: string;
password: HashPassword; password: HashPassword;
name?: String; name?: string;
address?: Address; address?: Address;
privateKey?: Key; privateKey?: Key;
publicKey?: Key; publicKey?: Key;
preSharedKey?: String; preSharedKey?: string;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
enabled: Boolean; enabled: boolean;
}; };

8
src/server/api/account/new.post.ts

@ -1,12 +1,12 @@
import { DatabaseError } from '~/ports/database'; import { DatabaseError } from '~/ports/database';
type Request = { username: string; password: string };
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'application/json'); setHeader(event, 'Content-Type', 'application/json');
try { try {
// TODO use zod const { username, password } = await readValidatedBody(
const { username, password } = await readBody<Request>(event); event,
validateZod(passwordType)
);
await Database.newUserWithPassword(username, password); await Database.newUserWithPassword(username, password);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {

2
src/server/api/lang.get.ts

@ -1,4 +1,4 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'application/json'); setHeader(event, 'Content-Type', 'application/json');
return await Database.getLang(); return Database.getLang();
}); });

10
src/server/api/release.get.ts

@ -1,8 +1,14 @@
export default defineEventHandler(async () => { export default defineEventHandler(async () => {
const release = Number.parseInt(RELEASE, 10); const system = await Database.getSystem();
if (!system)
throw createError({
statusCode: 400,
statusMessage: 'Invalid',
});
const latestRelease = await fetchLatestRelease(); const latestRelease = await fetchLatestRelease();
return { return {
currentRelease: release, currentRelease: system.release,
latestRelease: latestRelease, latestRelease: latestRelease,
}; };
}); });

6
src/server/api/session.get.ts

@ -1,11 +1,9 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const session = await useWGSession(event); const session = await useWGSession(event);
const authenticated = REQUIRES_PASSWORD const authenticated = session.data.authenticated === true;
? session.data.authenticated === true
: true;
return { return {
requiresPassword: REQUIRES_PASSWORD, requiresPassword: true,
authenticated, authenticated,
}; };
}); });

19
src/server/api/session.post.ts

@ -3,34 +3,27 @@ import type { SessionConfig } from 'h3';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { username, password, remember } = await readValidatedBody( const { username, password, remember } = await readValidatedBody(
event, event,
validateZod(passwordType) validateZod(credentialsType)
); );
if (!REQUIRES_PASSWORD) {
// if no password is required, the API should never be called.
// Do not automatically authenticate the user.
throw createError({
statusCode: 401,
statusMessage: 'Invalid state',
});
}
const users = await Database.getUsers(); const users = await Database.getUsers();
const user = users.find((user) => user.username == username); const user = users.find((user) => user.username == username);
if (!user) if (!user)
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: 'User with username does not exist', statusMessage: 'Incorrect credentials',
}); });
const userHashPassword = user.password; const userHashPassword = user.password;
if (!isPasswordValid(password, userHashPassword)) { if (!isPasswordValid(password, userHashPassword)) {
throw createError({ throw createError({
statusCode: 401, statusCode: 401,
statusMessage: 'Incorrect Password', statusMessage: 'Incorrect credentials',
}); });
} }
// TODO: timing againts timing attack
const conf: SessionConfig = SESSION_CONFIG; const conf: SessionConfig = SESSION_CONFIG;
if (MAX_AGE && remember) { if (MAX_AGE && remember) {
conf.cookie = { conf.cookie = {
@ -50,5 +43,5 @@ export default defineEventHandler(async (event) => {
SERVER_DEBUG(`New Session: ${data.id}`); SERVER_DEBUG(`New Session: ${data.id}`);
return { success: true, requiresPassword: REQUIRES_PASSWORD }; return { success: true, requiresPassword: true };
}); });

4
src/server/middleware/auth.ts

@ -2,12 +2,12 @@ export default defineEventHandler(async (event) => {
const url = getRequestURL(event); const url = getRequestURL(event);
const session = await useWGSession(event); const session = await useWGSession(event);
if (url.pathname === '/login') { if (url.pathname === '/login') {
if (!REQUIRES_PASSWORD || session.data.authenticated) { if (session.data.authenticated) {
return sendRedirect(event, '/', 302); return sendRedirect(event, '/', 302);
} }
} }
if (url.pathname === '/') { if (url.pathname === '/') {
if (!session.data.authenticated && REQUIRES_PASSWORD) { if (!session.data.authenticated) {
return sendRedirect(event, '/login', 302); return sendRedirect(event, '/login', 302);
} }
} }

1
src/server/middleware/session.ts

@ -1,7 +1,6 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const url = getRequestURL(event); const url = getRequestURL(event);
if ( if (
!REQUIRES_PASSWORD ||
!url.pathname.startsWith('/api/') || !url.pathname.startsWith('/api/') ||
url.pathname === '/api/account/new' || url.pathname === '/api/account/new' ||
url.pathname === '/api/session' || url.pathname === '/api/session' ||

4
src/server/utils/config.ts

@ -1,10 +1,7 @@
import type { SessionConfig } from 'h3'; import type { SessionConfig } from 'h3';
import packageJSON from '../../package.json';
import debug from 'debug'; import debug from 'debug';
const version = packageJSON.release.version;
export const RELEASE = version;
export const PORT = process.env.PORT || '51821'; export const PORT = process.env.PORT || '51821';
export const WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0'; export const WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0';
export const MAX_AGE = process.env.MAX_AGE export const MAX_AGE = process.env.MAX_AGE
@ -63,7 +60,6 @@ export const ENABLE_PROMETHEUS_METRICS =
export const PROMETHEUS_METRICS_PASSWORD = export const PROMETHEUS_METRICS_PASSWORD =
process.env.PROMETHEUS_METRICS_PASSWORD; process.env.PROMETHEUS_METRICS_PASSWORD;
export const REQUIRES_PASSWORD = true;
export const REQUIRES_PROMETHEUS_PASSWORD = !!PROMETHEUS_METRICS_PASSWORD; export const REQUIRES_PROMETHEUS_PASSWORD = !!PROMETHEUS_METRICS_PASSWORD;
export const SESSION_CONFIG = { export const SESSION_CONFIG = {

8
src/server/utils/password.ts

@ -3,7 +3,7 @@ import bcrypt from 'bcryptjs';
/** /**
* Checks if `password` matches the user password. * Checks if `password` matches the user password.
* *
* @param {string} password String to test * @param {string} password string to test
* @returns {boolean} `true` if matching user password, otherwise `false` * @returns {boolean} `true` if matching user password, otherwise `false`
*/ */
export function isPasswordValid(password: string, hash: string): boolean { export function isPasswordValid(password: string, hash: string): boolean {
@ -37,11 +37,11 @@ export function isPasswordStrong(password: string): boolean {
} }
/** /**
* Hashes a password using the bcrypt algorithm. * Hashes a password.
* *
* @param {string} password - The plaintext password to hash * @param {string} password - The plaintext password to hash
* @returns {string} The bcrypt hash of the password * @returns {string} The hash of the password
*/ */
export function hashPasswordWithBcrypt(password: string): string { export function hashPassword(password: string): string {
return bcrypt.hashSync(password, 12); return bcrypt.hashSync(password, 12);
} }

10
src/server/utils/types.ts

@ -85,7 +85,7 @@ export const fileType = z.object(
{ message: 'Body must be a valid object' } { message: 'Body must be a valid object' }
); );
export const passwordType = z.object( export const credentialsType = z.object(
{ {
username: username, username: username,
password: password, password: password,
@ -94,6 +94,14 @@ export const passwordType = z.object(
{ message: 'Body must be a valid object' } { message: 'Body must be a valid object' }
); );
export const passwordType = z.object(
{
username: username,
password: password,
},
{ message: 'Body must be a valid object' }
);
export function validateZod<T>(schema: ZodSchema<T>) { export function validateZod<T>(schema: ZodSchema<T>) {
return async (data: unknown) => { return async (data: unknown) => {
try { try {

Loading…
Cancel
Save