diff --git a/db.sqlite b/db.sqlite
index f92eaea..84e0994 100644
Binary files a/db.sqlite and b/db.sqlite differ
diff --git a/db/schema.ts b/db/schema.ts
index 4fbbfca..73496a6 100644
--- a/db/schema.ts
+++ b/db/schema.ts
@@ -86,6 +86,7 @@ export const campaignTable = table("campaign", {
name: text().notNull(),
description: text(),
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
+ joinby: text({ enum: [ 'link', 'invite' ] }).default('invite'),
});
export const campaignMembersTable = table("campaign_members", {
id: int().references(() => campaignTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
@@ -141,7 +142,7 @@ export const characterChoicesRelation = relations(characterChoicesTable, ({ one
export const campaignRelation = relations(campaignTable, ({ one, many }) => ({
members: many(campaignMembersTable),
characters: many(campaignCharactersTable),
- owner: one(usersTable),
+ owner: one(usersTable, { fields: [campaignTable.owner], references: [usersTable.id], }),
}));
export const campaignMembersRelation = relations(campaignMembersTable, ({ one }) => ({
campaign: one(campaignTable, { fields: [campaignMembersTable.id], references: [campaignTable.id], }),
diff --git a/layouts/default.vue b/layouts/default.vue
index 4adfb88..7018fdc 100644
--- a/layouts/default.vue
+++ b/layouts/default.vue
@@ -32,7 +32,7 @@
- Campagnes
+ Campagnes
diff --git a/pages/campaign/(automatic)/join.client.vue b/pages/campaign/(automatic)/join.client.vue
new file mode 100644
index 0000000..e69de29
diff --git a/pages/campaign/[id]/edit.client.vue b/pages/campaign/[id]/edit.client.vue
new file mode 100644
index 0000000..41a40c8
--- /dev/null
+++ b/pages/campaign/[id]/edit.client.vue
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/pages/campaign/[id]/index.client.vue b/pages/campaign/[id]/index.client.vue
new file mode 100644
index 0000000..41a40c8
--- /dev/null
+++ b/pages/campaign/[id]/index.client.vue
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/pages/campaign/index.client.vue b/pages/campaign/index.client.vue
new file mode 100644
index 0000000..30ab567
--- /dev/null
+++ b/pages/campaign/index.client.vue
@@ -0,0 +1,73 @@
+
+
+
+
+ d[any] - Mes personnages
+
+
+
+
+
+
+
+
+
+
+
+
{{ campaign.name }}
+
+
+ {{ campaign.members.length }} joueur{{ campaign.members.length === 1 ? '' : 's' }}
+
+
+
+
+
+
+
Editer
+
+
+
+ Quitter
+
+
+
+
+ Vous vous appretez à quitter "{{ campaign.name }}". Etes vous sûr ?
+
+
+
+
+
+
+
+
+
+
+
+ Vous n'avez pas encore rejoint de campagne
+ Créer ma campagne
+
+
+
+ Erreur de chargement
+ {{ error?.message }}
+
+
+
\ No newline at end of file
diff --git a/server/api/campaign.get.ts b/server/api/campaign.get.ts
new file mode 100644
index 0000000..ac24b0e
--- /dev/null
+++ b/server/api/campaign.get.ts
@@ -0,0 +1,22 @@
+import { eq } from 'drizzle-orm';
+import useDatabase from '~/composables/useDatabase';
+
+export default defineEventHandler(async (e) => {
+ const db = useDatabase();
+
+ const session = await getUserSession(e);
+
+ if(!session || !session.user)
+ {
+ setResponseStatus(e, 401);
+ return;
+ }
+
+ return 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 } }
+ },
+ }).sync();
+});
\ No newline at end of file
diff --git a/server/api/campaign.post.ts b/server/api/campaign.post.ts
new file mode 100644
index 0000000..a541a76
--- /dev/null
+++ b/server/api/campaign.post.ts
@@ -0,0 +1,47 @@
+import { z } from 'zod/v4';
+import useDatabase from '~/composables/useDatabase';
+import { campaignMembersTable, campaignTable } from '~/db/schema';
+import { CampaignValidation } from '~/shared/campaign.util';
+
+export default defineEventHandler(async (e) => {
+ const body = await readValidatedBody(e, CampaignValidation.extend({ id: z.unknown(), }).safeParse);
+ if(!body.success)
+ {
+ setResponseStatus(e, 400);
+ return body.error.message;
+ }
+
+ const session = await getUserSession(e);
+ if(!session.user || session.user.state !== 1)
+ {
+ setResponseStatus(e, 401);
+ return;
+ }
+
+ const db = useDatabase();
+ try
+ {
+ const id = db.transaction((tx) => {
+ const id = tx.insert(campaignTable).values({
+ name: body.data.name,
+ description: body.data.description,
+ owner: session.user!.id,
+ joinby: body.data.joinby,
+ }).returning({ id: campaignTable.id }).get().id;
+
+ tx.insert(campaignMembersTable).values({ id, rights: 'dm', user: session.user!.id }).run();
+
+ return id;
+ });
+
+ setResponseStatus(e, 201);
+ return id;
+ }
+ catch(_e)
+ {
+ console.error(_e);
+
+ setResponseStatus(e, 500);
+ return;
+ }
+});
\ No newline at end of file
diff --git a/server/api/campaign/[id].delete.ts b/server/api/campaign/[id].delete.ts
new file mode 100644
index 0000000..2efb971
--- /dev/null
+++ b/server/api/campaign/[id].delete.ts
@@ -0,0 +1,25 @@
+import { and, eq } from 'drizzle-orm';
+import useDatabase from '~/composables/useDatabase';
+import { campaignTable } from '~/db/schema';
+
+export default defineEventHandler(async (e) => {
+ const id = getRouterParam(e, "id");
+ if(!id) return setResponseStatus(e, 400);
+
+ const session = await getUserSession(e);
+ if(!session || !session.user) return setResponseStatus(e, 401);
+
+ const db = useDatabase();
+
+ try
+ {
+ const deleted = db.delete(campaignTable).where(and(eq(campaignTable.id, parseInt(id, 10)), eq(campaignTable.owner, session.user.id))).returning({ id: campaignTable.id }).get();
+
+ if(deleted) return setResponseStatus(e, 200);
+ else return setResponseStatus(e, 404);
+ }
+ catch(_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
new file mode 100644
index 0000000..cf1c662
--- /dev/null
+++ b/server/api/campaign/[id].get.ts
@@ -0,0 +1,32 @@
+import { eq } from 'drizzle-orm';
+import useDatabase from '~/composables/useDatabase';
+
+export default defineEventHandler(async (e) => {
+ const id = getRouterParam(e, "id");
+ if(!id)
+ {
+ setResponseStatus(e, 400);
+ return;
+ }
+
+ const session = await getUserSession(e);
+ if(!session || !session.user)
+ {
+ setResponseStatus(e, 401);
+ return;
+ }
+
+ 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 } }
+ },
+ 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;
+ 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
new file mode 100644
index 0000000..eeeaf50
--- /dev/null
+++ b/server/api/campaign/[id].post.ts
@@ -0,0 +1,55 @@
+import { eq } from 'drizzle-orm';
+import useDatabase from '~/composables/useDatabase';
+import { campaignTable } from '~/db/schema';
+import { CampaignValidation } from '~/shared/campaign.util';
+
+export default defineEventHandler(async (e) => {
+ const params = getRouterParam(e, "id");
+ if(!params)
+ {
+ setResponseStatus(e, 400);
+ return;
+ }
+ const id = parseInt(params, 10);
+
+ const body = await readValidatedBody(e, CampaignValidation.safeParse);
+ if(!body.success)
+ {
+ setResponseStatus(e, 400);
+ return body.error.message;
+ }
+
+ const db = useDatabase();
+ const old = db.select({ id: campaignTable.id, owner: campaignTable.owner }).from(campaignTable).where(eq(campaignTable.id, id)).get();
+
+ if(!old)
+ {
+ setResponseStatus(e, 404);
+ return;
+ }
+
+ const session = await getUserSession(e);
+ if(!session.user || old.owner !== session.user.id || session.user.state !== 1)
+ {
+ setResponseStatus(e, 401);
+ return;
+ }
+
+ try {
+ db.transaction((tx) => {
+ tx.update(campaignTable).set({
+ name: body.data.name,
+ description: body.data.description,
+ joinby: body.data.joinby,
+ }).where(eq(campaignTable.id, id)).run();
+ });
+ }
+ catch(_e)
+ {
+ setResponseStatus(e, 500);
+ return;
+ }
+
+ setResponseStatus(e, 200);
+ return;
+});
\ No newline at end of file
diff --git a/server/api/campaign/[id]/join.post.ts b/server/api/campaign/[id]/join.post.ts
new file mode 100644
index 0000000..bca417a
--- /dev/null
+++ b/server/api/campaign/[id]/join.post.ts
@@ -0,0 +1,37 @@
+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
new file mode 100644
index 0000000..8986fea
--- /dev/null
+++ b/server/api/campaign/[id]/leave.post.ts
@@ -0,0 +1,35 @@
+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 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: parseInt(id, 10) });
+
+ if(campaign && campaign.owner === session.user.id) return setResponseStatus(e, 403);
+
+ const deleted = db.delete(campaignMembersTable).where(and(eq(campaignMembersTable.id, parseInt(id, 10)), eq(campaignMembersTable.user, session.user.id))).returning({ id: campaignMembersTable.id }).get();
+
+ if(deleted) return setResponseStatus(e, 200);
+ else return setResponseStatus(e, 404);
+ }
+ catch(_e)
+ {
+ return setResponseStatus(e, 500);
+ }
+});
\ No newline at end of file
diff --git a/shared/campaign.util.ts b/shared/campaign.util.ts
new file mode 100644
index 0000000..980fbf4
--- /dev/null
+++ b/shared/campaign.util.ts
@@ -0,0 +1,8 @@
+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