Browse Source

move session timeout to session config, use new permission handler

pull/1619/head
Bernd Storath 3 months ago
parent
commit
ff2fcae6f9
  1. 43
      src/server/api/client/[clientId]/configuration.get.ts
  2. 19
      src/server/api/client/[clientId]/disable.post.ts
  3. 19
      src/server/api/client/[clientId]/enable.post.ts
  4. 19
      src/server/api/client/[clientId]/generateOneTimeLink.post.ts
  5. 19
      src/server/api/client/[clientId]/index.delete.ts
  6. 17
      src/server/api/client/[clientId]/index.get.ts
  7. 33
      src/server/api/client/[clientId]/index.post.ts
  8. 21
      src/server/api/client/[clientId]/qrcode.svg.get.ts
  9. 2
      src/server/api/client/index.get.ts
  10. 21
      src/server/api/client/index.post.ts
  11. 2
      src/server/api/session.get.ts
  12. 15
      src/server/api/session.post.ts
  13. 15
      src/server/api/wireguard/backup.get.ts
  14. 13
      src/server/api/wireguard/restore.put.ts
  15. 16
      src/server/database/repositories/general/schema.ts
  16. 1
      src/server/database/repositories/sessionConfig/schema.ts
  17. 2
      src/server/database/repositories/user/schema.ts
  18. 9
      src/server/database/repositories/user/service.ts
  19. 76
      src/server/utils/handler.ts
  20. 81
      src/server/utils/session.ts

43
src/server/api/client/[clientId]/configuration.get.ts

@ -1,20 +1,23 @@
export default defineEventHandler(async (event) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
const client = await WireGuard.getClient({ clientId });
const config = await WireGuard.getClientConfiguration({ clientId });
const configName = client.name
.replace(/[^a-zA-Z0-9_=+.-]/g, '-')
.replace(/(-{2,}|-$)/g, '-')
.replace(/-$/, '')
.substring(0, 32);
setHeader(
event,
'Content-Disposition',
`attachment; filename="${configName || clientId}.conf"`
);
setHeader(event, 'Content-Type', 'text/plain');
return config;
});
export default definePermissionEventHandler(
actions.CLIENT,
async ({ event }) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
const client = await Database.clients.get(clientId);
const config = await WireGuard.getClientConfiguration({ clientId });
const configName = client.name
.replace(/[^a-zA-Z0-9_=+.-]/g, '-')
.replace(/(-{2,}|-$)/g, '-')
.replace(/-$/, '')
.substring(0, 32);
setHeader(
event,
'Content-Disposition',
`attachment; filename="${configName || clientId}.conf"`
);
setHeader(event, 'Content-Type', 'text/plain');
return config;
}
);

19
src/server/api/client/[clientId]/disable.post.ts

@ -1,8 +1,11 @@
export default defineEventHandler(async (event) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
await WireGuard.disableClient({ clientId });
return { success: true };
});
export default definePermissionEventHandler(
actions.CLIENT,
async ({ event }) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
await WireGuard.disableClient({ clientId });
return { success: true };
}
);

19
src/server/api/client/[clientId]/enable.post.ts

@ -1,8 +1,11 @@
export default defineEventHandler(async (event) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
await WireGuard.enableClient({ clientId });
return { success: true };
});
export default definePermissionEventHandler(
actions.CLIENT,
async ({ event }) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
await WireGuard.enableClient({ clientId });
return { success: true };
}
);

19
src/server/api/client/[clientId]/generateOneTimeLink.post.ts

@ -1,8 +1,11 @@
export default defineEventHandler(async (event) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
await WireGuard.generateOneTimeLink({ clientId });
return { success: true };
});
export default definePermissionEventHandler(
actions.CLIENT,
async ({ event }) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
await WireGuard.generateOneTimeLink({ clientId });
return { success: true };
}
);

19
src/server/api/client/[clientId]/index.delete.ts

@ -1,8 +1,11 @@
export default defineEventHandler(async (event) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
await WireGuard.deleteClient({ clientId });
return { success: true };
});
export default definePermissionEventHandler(
actions.CLIENT,
async ({ event }) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
await WireGuard.deleteClient({ clientId });
return { success: true };
}
);

17
src/server/api/client/[clientId]/index.get.ts

@ -1,7 +1,10 @@
export default defineEventHandler(async (event) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
return WireGuard.getClient({ clientId });
});
export default definePermissionEventHandler(
actions.CLIENT,
async ({ event }) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
return WireGuard.getClient({ clientId });
}
);

33
src/server/api/client/[clientId]/index.post.ts

@ -1,18 +1,21 @@
export default defineEventHandler(async (event) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
export default definePermissionEventHandler(
actions.CLIENT,
async ({ event }) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
const data = await readValidatedBody(
event,
validateZod(clientUpdateType, event)
);
const data = await readValidatedBody(
event,
validateZod(clientUpdateType, event)
);
await WireGuard.updateClient({
clientId,
client: data,
});
await WireGuard.updateClient({
clientId,
client: data,
});
return { success: true };
});
return { success: true };
}
);

21
src/server/api/client/[clientId]/qrcode.svg.get.ts

@ -1,9 +1,12 @@
export default defineEventHandler(async (event) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
const svg = await WireGuard.getClientQRCodeSVG({ clientId });
setHeader(event, 'Content-Type', 'image/svg+xml');
return svg;
});
export default definePermissionEventHandler(
actions.CLIENT,
async ({ event }) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
const svg = await WireGuard.getClientQRCodeSVG({ clientId });
setHeader(event, 'Content-Type', 'image/svg+xml');
return svg;
}
);

2
src/server/api/client/index.get.ts

@ -1,3 +1,3 @@
export default defineEventHandler(() => {
export default definePermissionEventHandler(actions.CLIENT, () => {
return WireGuard.getClients();
});

21
src/server/api/client/index.post.ts

@ -1,11 +1,14 @@
import { ClientCreateSchema } from '#db/repositories/client/types';
export default defineEventHandler(async (event) => {
const { name, expiresAt } = await readValidatedBody(
event,
validateZod(ClientCreateSchema)
);
await Database.clients.create({ name, expiresAt });
await WireGuard.saveConfig();
return { success: true };
});
export default definePermissionEventHandler(
actions.CLIENT,
async ({ event }) => {
const { name, expiresAt } = await readValidatedBody(
event,
validateZod(ClientCreateSchema)
);
await Database.clients.create({ name, expiresAt });
await WireGuard.saveConfig();
return { success: true };
}
);

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

@ -7,7 +7,7 @@ export default defineEventHandler(async (event) => {
statusMessage: 'Not logged in',
});
}
const user = await Database.user.findById(session.data.userId);
const user = await Database.users.get(session.data.userId);
if (!user) {
throw createError({
statusCode: 404,

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

@ -4,7 +4,7 @@ export default defineEventHandler(async (event) => {
validateZod(credentialsType, event)
);
const users = await Database.user.findAll();
const users = await Database.users.getAll();
const user = users.find((user) => user.username == username);
if (!user)
throw createError({
@ -21,18 +21,7 @@ export default defineEventHandler(async (event) => {
});
}
const system = await Database.system.get();
const conf = { ...system.sessionConfig };
if (remember) {
conf.cookie = {
...(system.sessionConfig.cookie ?? {}),
maxAge: system.general.sessionTimeout,
};
}
const session = await useSession<WGSession>(event, conf);
const session = await useWGSession(event, remember);
const data = await session.update({
userId: user.id,

15
src/server/api/wireguard/backup.get.ts

@ -1,6 +1,9 @@
export default defineEventHandler(async (event) => {
const config = await WireGuard.backupConfiguration();
setHeader(event, 'Content-Disposition', 'attachment; filename="wg0.json"');
setHeader(event, 'Content-Type', 'text/json');
return config;
});
export default definePermissionEventHandler(
actions.ADMIN,
async ({ event }) => {
const config = await WireGuard.backupConfiguration();
setHeader(event, 'Content-Disposition', 'attachment; filename="wg0.json"');
setHeader(event, 'Content-Type', 'text/json');
return config;
}
);

13
src/server/api/wireguard/restore.put.ts

@ -1,5 +1,8 @@
export default defineEventHandler(async (event) => {
const { file } = await readValidatedBody(event, validateZod(fileType));
await WireGuard.restoreConfiguration(file);
return { success: true };
});
export default definePermissionEventHandler(
actions.ADMIN,
async ({ event }) => {
const { file } = await readValidatedBody(event, validateZod(fileType));
await WireGuard.restoreConfiguration(file);
return { success: true };
}
);

16
src/server/database/repositories/general/schema.ts

@ -1,16 +0,0 @@
import { sql } from 'drizzle-orm';
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const general = sqliteTable('general_table', {
// limit to one entry
id: int().primaryKey({ autoIncrement: false }).default(1),
//test: text().notNull(),
sessionTimeout: int('session_timeout').notNull(),
createdAt: text('created_at')
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
updatedAt: text('updated_at')
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`)
.$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
});

1
src/server/database/repositories/sessionConfig/schema.ts

@ -5,6 +5,7 @@ export const sessionConfig = sqliteTable('session_config_table', {
// limit to one entry
id: int().primaryKey({ autoIncrement: false }).default(1),
password: text().notNull(),
timeout: int().notNull(),
createdAt: text('created_at')
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),

2
src/server/database/repositories/user/schema.ts

@ -3,7 +3,7 @@ import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const user = sqliteTable('users_table', {
id: int().primaryKey({ autoIncrement: true }),
username: text().notNull(),
username: text().notNull().unique(),
password: text().notNull(),
email: text(),
name: text().notNull(),

9
src/server/database/repositories/user/service.ts

@ -9,6 +9,11 @@ function createPreparedStatement(db: DBType) {
findById: db.query.user
.findFirst({ where: eq(user.id, sql.placeholder('id')) })
.prepare(),
findByUsername: db.query.user
.findFirst({
where: eq(user.username, sql.placeholder('username')),
})
.prepare(),
};
}
@ -26,4 +31,8 @@ export class UserService {
async get(id: ID) {
return this.#statements.findById.all({ id });
}
async getByUsername(username: string) {
return this.#statements.findByUsername.all({ username });
}
}

76
src/server/utils/handler.ts

@ -25,79 +25,3 @@ export const definePermissionEventHandler = <
return await handler({ event, user });
});
};
/**
* @throws
*/
async function getCurrentUser(event: H3Event) {
const session = await getWGSession(event);
const authorization = getHeader(event, 'Authorization');
let user: UserType | undefined = undefined;
if (session.data.userId) {
// Handle if authenticating using Session
user = await Database.users.get(session.data.userId);
} else if (authorization) {
// Handle if authenticating using Header
const [method, value] = authorization.split(' ');
// Support Basic Authentication
// TODO: support personal access token or similar
if (method !== 'Basic' || !value) {
throw createError({
statusCode: 401,
statusMessage: 'Session failed',
});
}
const basicValue = Buffer.from(value, 'base64').toString('utf-8');
// Split by first ":"
const index = basicValue.indexOf(':');
const userId = basicValue.substring(0, index);
const password = basicValue.substring(index + 1);
if (!userId || !password) {
throw createError({
statusCode: 401,
statusMessage: 'Session failed',
});
}
const foundUser = await Database.users.get(Number.parseInt(userId));
if (!foundUser) {
throw createError({
statusCode: 401,
statusMessage: 'Session failed',
});
}
const userHashPassword = foundUser.password;
const passwordValid = await isPasswordValid(password, userHashPassword);
if (!passwordValid) {
throw createError({
statusCode: 401,
statusMessage: 'Incorrect Password',
});
}
user = foundUser;
}
if (!user) {
throw createError({
statusCode: 401,
statusMessage: 'Session failed. User not found',
});
}
if (!user.enabled) {
throw createError({
statusCode: 403,
statusMessage: 'User is disabled',
});
}
return user;
}

81
src/server/utils/session.ts

@ -1,5 +1,6 @@
import type { H3Event } from 'h3';
import type { ID } from '#db/schema';
import type { UserType } from '#db/repositories/user/types';
export type WGSession = Partial<{
userId: ID;
@ -7,12 +8,12 @@ export type WGSession = Partial<{
const name = 'wg-easy';
export async function useWGSession(event: H3Event) {
export async function useWGSession(event: H3Event, rememberMe = false) {
const sessionConfig = await Database.sessionConfig.get();
return useSession<WGSession>(event, {
password: sessionConfig.password,
name,
cookie: {},
cookie: { maxAge: rememberMe ? sessionConfig.timeout : undefined },
});
}
@ -24,3 +25,79 @@ export async function getWGSession(event: H3Event) {
cookie: {},
});
}
/**
* @throws
*/
export async function getCurrentUser(event: H3Event) {
const session = await getWGSession(event);
const authorization = getHeader(event, 'Authorization');
let user: UserType | undefined = undefined;
if (session.data.userId) {
// Handle if authenticating using Session
user = await Database.users.get(session.data.userId);
} else if (authorization) {
// Handle if authenticating using Header
const [method, value] = authorization.split(' ');
// Support Basic Authentication
// TODO: support personal access token or similar
if (method !== 'Basic' || !value) {
throw createError({
statusCode: 401,
statusMessage: 'Session failed',
});
}
const basicValue = Buffer.from(value, 'base64').toString('utf-8');
// Split by first ":"
const index = basicValue.indexOf(':');
const username = basicValue.substring(0, index);
const password = basicValue.substring(index + 1);
if (!username || !password) {
throw createError({
statusCode: 401,
statusMessage: 'Session failed',
});
}
const foundUser = await Database.users.getByUsername(username);
if (!foundUser) {
throw createError({
statusCode: 401,
statusMessage: 'Session failed',
});
}
const userHashPassword = foundUser.password;
const passwordValid = await isPasswordValid(password, userHashPassword);
if (!passwordValid) {
throw createError({
statusCode: 401,
statusMessage: 'Incorrect Password',
});
}
user = foundUser;
}
if (!user) {
throw createError({
statusCode: 401,
statusMessage: 'Session failed. User not found',
});
}
if (!user.enabled) {
throw createError({
statusCode: 403,
statusMessage: 'User is disabled',
});
}
return user;
}

Loading…
Cancel
Save