- Vous vous appretez à quitter "{{ campaign.name }}". Etes vous sûr ?
-
-
Non
-
leaveCampaign(campaign.id)" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Oui
+
+
+
+
+
+
+
{{ campaign.name }}
+
+
+ {{ campaign.members.length }} joueur{{ campaign.members.length === 1 ? '' : 's' }}
-
-
-
+
+
+
+
+
+
+
+ {{ user.id !== campaign.owner.id ? 'Quitter' : 'Supprimer' }}
+
+
+
+
+ Vous vous appretez à {{ user.id !== campaign.owner.id ? 'quitter' : 'supprimer' }} "{{ campaign.name }}". Etes vous sûr ?
+
+
Non
+
user?.id !== campaign.owner.id ? leaveCampaign(campaign.id) : removeCampaign(campaign.id)" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Oui
+
+
+
+
+
+
+
+
+
+
+
{{ campaign.name }}
+
+
+ {{ campaign.members.length }} joueur{{ campaign.members.length === 1 ? '' : 's' }}
+
+
+
+
+
+
+
+
Archives
+
+
+
+
+
+
{{ campaign.name }}
+
+
+ {{ campaign.members.length }} joueur{{ campaign.members.length === 1 ? '' : 's' }}
+
+
+
+
+
+
+
+
+ {{ user.id !== campaign.owner.id ? 'Quitter' : 'Supprimer' }}
+
+
+
+
+ Vous vous appretez à {{ user.id !== campaign.owner.id ? 'quitter' : 'supprimer' }} "{{ campaign.name }}". Etes vous sûr ?
+
+
Non
+
user?.id !== campaign.owner.id ? leaveCampaign(campaign.id) : removeCampaign(campaign.id)" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Oui
+
+
+
+
+
+
+
+
+
+
+
{{ campaign.name }}
+
+
+ {{ campaign.members.length }} joueur{{ campaign.members.length === 1 ? '' : 's' }}
+
+
+
+
+
Vous n'avez pas encore rejoint de campagne
- Créer ma campagne
+ focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50 py-2 px-4" @click="create">Créer ma campagne
+
diff --git a/server/api/campaign.get.ts b/server/api/campaign.get.ts
index ac24b0e..03ce3c5 100644
--- a/server/api/campaign.get.ts
+++ b/server/api/campaign.get.ts
@@ -6,17 +6,15 @@ export default defineEventHandler(async (e) => {
const session = await getUserSession(e);
- if(!session || !session.user)
- {
- setResponseStatus(e, 401);
- return;
- }
+ if(!session || !session.user) return setResponseStatus(e, 401);
- return db.query.campaignTable.findMany({
+ const data = db.query.campaignTable.findMany({
with: {
members: { with: { member: { columns: { username: true, id: true } } }, columns: { } },
characters: { where: ({ id }) => eq(id, session.user!.id), columns: { character: true } },
- owner: { columns: { username: true, id: true } }
+ owner: { columns: { username: true, id: true } },
},
}).sync();
+
+ return data.filter(e => e.owner.id === session.user!.id || e.members.find(e => e.member?.id === session.user!.id));
});
\ No newline at end of file
diff --git a/server/api/campaign.post.ts b/server/api/campaign.post.ts
index a541a76..5a577dc 100644
--- a/server/api/campaign.post.ts
+++ b/server/api/campaign.post.ts
@@ -1,7 +1,9 @@
+import { eq } from 'drizzle-orm';
import { z } from 'zod/v4';
import useDatabase from '~/composables/useDatabase';
import { campaignMembersTable, campaignTable } from '~/db/schema';
import { CampaignValidation } from '~/shared/campaign.util';
+import { cryptURI } from '~/shared/general.util';
export default defineEventHandler(async (e) => {
const body = await readValidatedBody(e, CampaignValidation.extend({ id: z.unknown(), }).safeParse);
@@ -26,8 +28,10 @@ export default defineEventHandler(async (e) => {
name: body.data.name,
description: body.data.description,
owner: session.user!.id,
- joinby: body.data.joinby,
+ link: '',
}).returning({ id: campaignTable.id }).get().id;
+
+ tx.update(campaignTable).set({ link: cryptURI('campaign', id) }).where(eq(campaignTable.id, id)).run();
tx.insert(campaignMembersTable).values({ id, rights: 'dm', user: session.user!.id }).run();
diff --git a/server/api/campaign/[id].delete.ts b/server/api/campaign/[id].delete.ts
index 2efb971..59a92bf 100644
--- a/server/api/campaign/[id].delete.ts
+++ b/server/api/campaign/[id].delete.ts
@@ -20,6 +20,7 @@ export default defineEventHandler(async (e) => {
}
catch(_e)
{
+ console.error(_e);
return setResponseStatus(e, 500);
}
});
\ No newline at end of file
diff --git a/server/api/campaign/[id].get.ts b/server/api/campaign/[id].get.ts
index cf1c662..c11dd39 100644
--- a/server/api/campaign/[id].get.ts
+++ b/server/api/campaign/[id].get.ts
@@ -1,4 +1,4 @@
-import { eq } from 'drizzle-orm';
+import { eq, not, sql } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
export default defineEventHandler(async (e) => {
@@ -19,14 +19,18 @@ export default defineEventHandler(async (e) => {
const db = useDatabase();
const data = db.query.campaignTable.findFirst({
with: {
- members: { with: { member: { columns: { username: true, id: true } } }, columns: { } },
- characters: { columns: { character: true } },
- owner: { columns: { username: true, id: true } }
+ members: { with: { member: { columns: { username: true, id: true } } }, columns: { }, where: ({ rights }) => not(eq(rights, 'dm')), extras: { 'characters': sql`NULL`.as('characters') } },
+ characters: { with: { character: { columns: { id: true, name: true, owner: true } } } },
+ owner: { columns: { username: true, id: true } },
},
where: ({ id: _id }) => eq(_id, parseInt(id, 10)),
}).sync();
- if(data && (data.owner === session.user.id || data.members.find(e => e.member?.id === session.user!.id))) return data;
+ if(data && (data.owner.id === session.user.id || data.members.find(e => e.member?.id === session.user!.id)))
+ {
+ data.members.forEach(e => e.characters = data.characters.filter(_e => _e.character?.owner === e.member?.id))
+ return data;
+ }
else if(!data) return setResponseStatus(e, 404);
else return setResponseStatus(e, 403);
});
\ No newline at end of file
diff --git a/server/api/campaign/[id].post.ts b/server/api/campaign/[id].post.ts
index eeeaf50..8bf828c 100644
--- a/server/api/campaign/[id].post.ts
+++ b/server/api/campaign/[id].post.ts
@@ -40,7 +40,6 @@ export default defineEventHandler(async (e) => {
tx.update(campaignTable).set({
name: body.data.name,
description: body.data.description,
- joinby: body.data.joinby,
}).where(eq(campaignTable.id, id)).run();
});
}
diff --git a/server/api/campaign/[id]/join.post.ts b/server/api/campaign/[id]/join.post.ts
deleted file mode 100644
index bca417a..0000000
--- a/server/api/campaign/[id]/join.post.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { and, eq, sql } from "drizzle-orm";
-import { campaignMembersTable, campaignTable } from "~/db/schema";
-
-export default defineEventHandler(async (e) => {
- const _id = getRouterParam(e, "id");
- if(!_id)
- {
- setResponseStatus(e, 400);
- return;
- }
- const id = parseInt(_id, 10);
-
- const session = await getUserSession(e);
- if(!session || !session.user)
- {
- setResponseStatus(e, 401);
- return;
- }
-
- const db = useDatabase();
- try
- {
- const campaign = db.select({ owner: campaignTable.owner }).from(campaignTable).where(and(eq(campaignTable.id, sql.placeholder('id')))).get({ id: id });
- if(campaign && campaign.owner === session.user.id) return setResponseStatus(e, 403);
-
- const members = db.select({ id: campaignMembersTable.user }).from(campaignMembersTable).where(and(eq(campaignMembersTable.id, sql.placeholder('id')), eq(campaignMembersTable.user, sql.placeholder('user')))).get({ id: id, user: session.user.id });
- if(members) return setResponseStatus(e, 403);
-
- db.insert(campaignMembersTable).values({ id, rights: 'player', user: session.user!.id });
-
- return setResponseStatus(e, 200);
- }
- catch(_e)
- {
- return setResponseStatus(e, 500);
- }
-});
\ No newline at end of file
diff --git a/server/api/campaign/[id]/leave.post.ts b/server/api/campaign/[id]/leave.post.ts
index 8986fea..28a3fb2 100644
--- a/server/api/campaign/[id]/leave.post.ts
+++ b/server/api/campaign/[id]/leave.post.ts
@@ -1,4 +1,5 @@
import { and, eq, sql } from "drizzle-orm";
+import useDatabase from "~/composables/useDatabase";
import { campaignMembersTable, campaignTable } from "~/db/schema";
export default defineEventHandler(async (e) => {
diff --git a/server/middleware/compress.ts b/server/middleware/compress.ts
deleted file mode 100644
index 58af3c3..0000000
--- a/server/middleware/compress.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { defineEventHandler, setResponseHeader } from 'h3';
-
-export default defineEventHandler(async (event) => {
- const acceptEncoding = event.headers.get('accept-encoding') || '';
- if (!acceptEncoding.includes('zstd')) return;
-
- const _end = event.node.res.end;
- //@ts-expect-error
- event.node.res.end = async (body: any, ...args: any[]) => {
- const buffer = typeof body === "string" ? new TextEncoder().encode(body) : body;
-
- if(buffer)
- {
- setResponseHeader(event, "Content-Encoding", "zstd");
- setResponseHeader(event, "Vary", "Accept-Encoding");
- //@ts-expect-error
- _end.call(event.node.res, await Bun.zstdCompress(buffer), ...args);
- }
- else
- {
- //@ts-expect-error
- _end.call(event.node.res, body, ...args);
- }
- };
-});
\ No newline at end of file
diff --git a/server/routes/campaign/join/[link].get.ts b/server/routes/campaign/join/[link].get.ts
new file mode 100644
index 0000000..b16beb4
--- /dev/null
+++ b/server/routes/campaign/join/[link].get.ts
@@ -0,0 +1,37 @@
+import { and, eq, sql } from "drizzle-orm";
+import useDatabase from "~/composables/useDatabase";
+import { campaignMembersTable, campaignTable } from "~/db/schema";
+import { decryptURI } from "~/shared/general.util";
+
+export default defineEventHandler(async (e) => {
+ const link = getRouterParam(e, "link");
+ if(!link) return setResponseStatus(e, 400);
+
+ const id = decryptURI(decodeURIComponent(link), 'campaign');
+
+ if(!id) return sendRedirect(e, `/campaign`);
+
+ const session = await getUserSession(e);
+ if(!session || !session.user) return sendRedirect(e, `/user/login`);
+
+ const db = useDatabase();
+ try
+ {
+ const campaign = db.select({ owner: campaignTable.owner }).from(campaignTable).where(and(eq(campaignTable.id, sql.placeholder('id')), eq(campaignTable.link, sql.placeholder('link')))).get({ id, link: decodeURIComponent(link) });
+
+ if(!campaign) return setResponseStatus(e, 404);
+ if(campaign && campaign.owner === session.user.id) return sendRedirect(e, `/campaign/${id}`);
+
+ const members = db.select({ id: campaignMembersTable.user }).from(campaignMembersTable).where(and(eq(campaignMembersTable.id, sql.placeholder('id')), eq(campaignMembersTable.user, sql.placeholder('user')))).get({ id: id, user: session.user.id });
+
+ if(members) return sendRedirect(e, `/campaign/${id}`);
+
+ db.insert(campaignMembersTable).values({ id, rights: 'player', user: session.user!.id }).run();
+
+ return sendRedirect(e, `/campaign/${id}`);
+ }
+ catch(_e)
+ {
+ return sendRedirect(e, `/campaign`);
+ }
+})
\ No newline at end of file
diff --git a/server/routes/ws/campaign/[id].ts b/server/routes/ws/campaign/[id].ts
new file mode 100644
index 0000000..bf1e6e0
--- /dev/null
+++ b/server/routes/ws/campaign/[id].ts
@@ -0,0 +1,22 @@
+export default defineWebSocketHandler({
+ message(peer, message) {
+
+ },
+ 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}`, true);
+
+
+ },
+ 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.unsubscribe(`campaigns/${id}`);
+ },
+ error(peer, error) {
+ console.error(error);
+ }
+})
\ No newline at end of file
diff --git a/shared/campaign.util.ts b/shared/campaign.util.ts
index 980fbf4..a7ae0aa 100644
--- a/shared/campaign.util.ts
+++ b/shared/campaign.util.ts
@@ -3,6 +3,13 @@ import { z } from "zod/v4";
export const CampaignValidation = z.object({
id: z.number(),
name: z.string().nonempty(),
- description: z.string(),
- joinby: z.enum([ 'link', 'invite' ]),
-});
\ No newline at end of file
+ description: z.string()
+});
+
+class CampaignSheet
+{
+ constructor()
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/shared/character.util.ts b/shared/character.util.ts
index d81cc2f..40f9223 100644
--- a/shared/character.util.ts
+++ b/shared/character.util.ts
@@ -44,6 +44,7 @@ export const defaultCharacter: Character = {
exhaustion: 0,
sickness: [],
poisons: [],
+ money: 0,
},
owner: -1,
@@ -563,6 +564,8 @@ export class CharacterBuilder extends CharacterCompiler
{
document.title = `d[any] - Edition de nouveau personnage`;
+ this.id = 'new';
+ this._character.id = -1;
this.render();
this.display(0);
}
@@ -628,14 +631,21 @@ export class CharacterBuilder extends CharacterCompiler
{
if(this.id === 'new' || this.id === '-1')
{
- //@ts-ignore
- this.id = this._character.id = this._result.id = await useRequestFetch()(`/api/character`, {
+ const result = await useRequestFetch()(`/api/character`, {
method: 'post',
body: this._character,
onResponseError: (e) => {
Toaster.add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', closeable: true, duration: 25000, timer: true });
+ this.id = 'new';
}
});
+
+ if(result !== undefined)
+ {
+ this._character.id = this._result.id = result as number;
+ this.id = result.toString();
+ }
+
Toaster.add({ content: 'Personnage créé', type: 'success', duration: 25000, timer: true });
useRouter().replace({ name: 'character-id-edit', params: { id: this.id } })
if(leave) useRouter().push({ name: 'character-id', params: { id: this.id } });
diff --git a/shared/content.util.ts b/shared/content.util.ts
index 3090ee5..2d359d8 100644
--- a/shared/content.util.ts
+++ b/shared/content.util.ts
@@ -139,6 +139,7 @@ export class Content
try
{
Content._overview = parse>>(overview);
+ Content._reverseMapping = Object.values(Content._overview).reduce((p, v) => { p[v.path] = v.id; return p; }, {} as Record);
await Content.pull();
}
catch(e)
@@ -245,16 +246,14 @@ export class Content
if(force || !_overview || new Date(_overview.timestamp) < new Date(file.timestamp))
{
Content._overview[file.id] = file;
+ Content._reverseMapping[file.path] = file.id;
Content.queue.queue(() => {
return useRequestFetch()(`/api/file/content/${file.id}`, { cache: 'no-cache' }).then(async (content: string | undefined | null) => {
if(content)
{
if(file.type !== 'folder')
- {
Content.queue.queue(() => Content.write(file.id, content, { create: true }));
- //Content.queue.queue(() => Content.write('storage/' + file.path + (file.type === 'canvas' ? '.canvas' : '.md'), Content.toString({ ...file, content: Content.fromString(file, content) }), { create: true }));
- }
}
else
Content._overview[file.id]!.error = true;
diff --git a/shared/general.util.ts b/shared/general.util.ts
index 9f528dd..26a6990 100644
--- a/shared/general.util.ts
+++ b/shared/general.util.ts
@@ -68,4 +68,36 @@ export function clamp(x: number, min: number, max: number): number
export function lerp(x: number, a: number, b: number): number
{
return (1-x)*a+x*b;
+}
+// The value position is randomized
+// The metadata separator is randomized as a letter (to avoid collision with numbers)
+// The URI is (| == picked separator) |v_length|first part of the hash + seed + second part of the hash|v_pos|seed as hex.
+// Every number are converted to string as hexadecimal values
+export function cryptURI(key: string, value: number, seed?: number): string
+{
+ const _seed = seed ?? Date.now();
+ const hash = Bun.hash(key + value.toString(), _seed).toString(16);
+ const pos = Math.floor(Math.random() * hash.length);
+ const separator = String.fromCharCode(Math.floor(Math.random() * 26 + 96));
+
+ return Bun.zstdCompressSync(separator + value.toString(16).length + separator + hash.substring(0, pos) + value + hash.substring(pos) + separator + pos + separator + _seed.toString(16), { level: 1 }).toString('base64');
+}
+export function decryptURI(uri: string, key: string): number | undefined
+{
+ const decoder = new TextDecoder();
+ const _uri = decoder.decode(Bun.zstdDecompressSync(Buffer.from(uri, 'base64')));
+
+ const separator = _uri.charAt(0);
+ const length = parseInt(_uri.substring(1, _uri.indexOf(separator, 1)), 10);
+ const seed = parseInt(_uri.substring(_uri.lastIndexOf(separator) + 1), 16);
+ const pos = parseInt(_uri.substring(_uri.lastIndexOf(separator, _uri.length - seed.toString(16).length - 2) + 1, _uri.lastIndexOf(separator)), 10);
+ const _hash = _uri.substring(2 + length.toString(10).length, _uri.length - (2 + seed.toString(16).length + pos.toString(10).length));
+
+ const value = _hash.substring(pos, pos + length);
+ const hash = _hash.substring(0, pos) + _hash.substring(pos + length);
+
+ if(Bun.hash(key + value, seed).toString(16) === hash)
+ return parseInt(value, 10);
+ else
+ return undefined;
}
\ No newline at end of file