WebSocket API, new ID/encrypt/decrypt algorithm.

This commit is contained in:
Clément Pons
2025-11-18 17:54:11 +01:00
parent 2a158be3fa
commit 7a40f8abac
26 changed files with 2303 additions and 293 deletions

View File

@@ -1,7 +1,7 @@
import { eq } from 'drizzle-orm';
import { z } from 'zod/v4';
import useDatabase from '~/composables/useDatabase';
import { campaignMembersTable, campaignTable } from '~/db/schema';
import { campaignTable } from '~/db/schema';
import { CampaignValidation } from '#shared/campaign.util';
import { cryptURI } from '#shared/general.util';
@@ -26,13 +26,15 @@ export default defineEventHandler(async (e) => {
const id = db.transaction((tx) => {
const id = tx.insert(campaignTable).values({
name: body.data.name,
description: body.data.description,
public_notes: body.data.public_notes,
owner: session.user!.id,
dm_notes: body.data.dm_notes,
settings: body.data.settings,
link: '',
}).returning({ id: campaignTable.id }).get().id;
tx.update(campaignTable).set({ link: cryptURI('campaign', id) }).where(eq(campaignTable.id, id)).run();
return id;
});

View File

@@ -24,7 +24,7 @@ export default defineEventHandler(async (e) => {
members: { with: { member: { columns: { username: true, id: true } } }, columns: { id: false, user: false } },
characters: { with: { character: { columns: { id: true, name: true, owner: true } } }, columns: { character: false } },
owner: { columns: { username: true, id: true } },
logs: { columns: { details: true, from: true, timestamp: true, type: true }, orderBy: ({ timestamp }) => timestamp },
logs: { columns: { details: true, target: true, timestamp: true, type: true }, orderBy: ({ timestamp }) => timestamp },
},
where: ({ id: _id }) => eq(_id, parseInt(id, 10)),
}).sync();

View File

@@ -39,7 +39,9 @@ export default defineEventHandler(async (e) => {
db.transaction((tx) => {
tx.update(campaignTable).set({
name: body.data.name,
description: body.data.description,
public_notes: body.data.public_notes,
dm_notes: body.data.dm_notes,
settings: body.data.settings,
}).where(eq(campaignTable.id, id)).run();
});
}

View File

@@ -33,7 +33,7 @@ export default defineEventHandler(async (e) => {
columns: { username: true }
},
campaign: {
columns: { character: false, id: false, },
columns: { character: false, id: true, },
with: {
campaign: {
columns: { owner: true, },
@@ -70,6 +70,8 @@ export default defineEventHandler(async (e) => {
owner: character.owner,
username: character.user.username,
visibility: character.visibility,
campaign: character.campaign?.id,
} as Character;
}

View File

@@ -1,30 +1,36 @@
const TRIGGER_CHAR = {
PING: String.fromCharCode(0x02),
PONG: String.fromCharCode(0x03),
STATUS: String.fromCharCode(0x04),
};
import type { SocketMessage } from "#shared/websocket.util";
import type { User } from "~/types/auth";
export default defineWebSocketHandler({
message(peer, message) {
switch(message.rawData)
const data = message.json<SocketMessage>();
switch(data.type)
{
case TRIGGER_CHAR.PING:
peer.send(TRIGGER_CHAR.PONG);
return;
default:
case 'PING':
peer.send(JSON.stringify({ type: 'PONG' }));
return;
default: return;
}
},
open(peer) {
async open(peer) {
const id = new URL(peer.request.url).pathname.split('/').slice(-1)[0];
if(!id) return peer.close();
peer.subscribe(`campaigns/${id}`);
peer.publish(`campaigns/${id}`, `${TRIGGER_CHAR.STATUS}`);
const session = await getUserSession(peer);
if(!session ||!session.user) return peer.close();
peer.context.user = session.user;
const topic = `campaigns/${id}`;
peer.subscribe(topic);
peer.publish(topic, { type: 'user', data: [{ user: (peer.context.user as User).id, status: true }] });
peer.send({ type: 'user', data: peer.peers.values().filter(e => e.topics.has(topic)).map(e => ({ user: (e.context.user as User).id, status: true })).toArray() })
},
close(peer, details) {
const id = new URL(peer.request.url).pathname.split('/').slice(-1)[0];
if(!id) return peer.close();
peer.publish(`campaigns/${id}`, false);
peer.publish(`campaigns/${id}`, { type: 'user', data: [{ user: (peer.context.user as User).id, status: false }] });
peer.unsubscribe(`campaigns/${id}`);
}
});

View File

@@ -4,6 +4,7 @@ import { defu } from 'defu'
import { createHooks } from 'hookable'
import { useRuntimeConfig } from '#imports'
import type { UserSession, UserSessionRequired } from '~/types/auth'
import type { CompatEvent } from '~~/shared/websocket.util'
export interface SessionHooks {
/**
@@ -11,11 +12,11 @@ export interface SessionHooks {
* - Add extra properties to the session
* - Throw an error if the session could not be verified (with a database for example)
*/
fetch: (session: UserSessionRequired, event: H3Event) => void | Promise<void>
fetch: (session: UserSessionRequired, event: H3Event | CompatEvent) => void | Promise<void>
/**
* Called before clearing the session
*/
clear: (session: UserSession, event: H3Event) => void | Promise<void>
clear: (session: UserSession, event: H3Event | CompatEvent) => void | Promise<void>
}
export const sessionHooks = createHooks<SessionHooks>()
@@ -25,7 +26,7 @@ export const sessionHooks = createHooks<SessionHooks>()
* @param event The Request (h3) event
* @returns The user session
*/
export async function getUserSession(event: H3Event) {
export async function getUserSession(event: H3Event | CompatEvent) {
const session = await _useSession(event);
if(!session.data || !session.data.id)
@@ -41,7 +42,7 @@ export async function getUserSession(event: H3Event) {
* @param data User session data, please only store public information since it can be decoded with API calls
* @see https://github.com/atinux/nuxt-auth-utils
*/
export async function setUserSession(event: H3Event, data: UserSession) {
export async function setUserSession(event: H3Event | CompatEvent, data: UserSession) {
const session = await _useSession(event)
await session.update(defu(data, session.data))
@@ -54,7 +55,7 @@ export async function setUserSession(event: H3Event, data: UserSession) {
* @param event The Request (h3) event
* @param data User session data, please only store public information since it can be decoded with API calls
*/
export async function replaceUserSession(event: H3Event, data: UserSession) {
export async function replaceUserSession(event: H3Event | CompatEvent, data: UserSession) {
const session = await _useSession(event)
await session.clear()
@@ -68,7 +69,7 @@ export async function replaceUserSession(event: H3Event, data: UserSession) {
* @param event The Request (h3) event
* @returns true if the session was cleared
*/
export async function clearUserSession(event: H3Event) {
export async function clearUserSession(event: H3Event | CompatEvent) {
const session = await _useSession(event)
await sessionHooks.callHookParallel('clear', session.data, event)
@@ -85,7 +86,7 @@ export async function clearUserSession(event: H3Event) {
* @param opts.message The message to use for the error (defaults to Unauthorized)
* @see https://github.com/atinux/nuxt-auth-utils
*/
export async function requireUserSession(event: H3Event, opts: { statusCode?: number, message?: string } = {}): Promise<UserSessionRequired> {
export async function requireUserSession(event: H3Event | CompatEvent, opts: { statusCode?: number, message?: string } = {}): Promise<UserSessionRequired> {
const userSession = await getUserSession(event)
if (!userSession.user) {
@@ -100,9 +101,9 @@ export async function requireUserSession(event: H3Event, opts: { statusCode?: nu
let sessionConfig: SessionConfig
function _useSession(event: H3Event) {
if (!sessionConfig) {
const runtimeConfig = useRuntimeConfig(event)
function _useSession(event: H3Event | CompatEvent) {
if (!sessionConfig && '__is_event__' in event) {
const runtimeConfig = useRuntimeConfig(event);
sessionConfig = runtimeConfig.session;
}