6
0
forked from jmug/cactoide

Compare commits

..

1 Commits

Author SHA1 Message Date
Levente Orban
fdc04502f9 feat: change the userId beahviour 2026-08-11 09:02:40 +02:00
19 changed files with 33 additions and 65 deletions

View File

@@ -27,17 +27,16 @@ export const handle: Handle = async ({ event, resolve }) => {
} }
const cactoideUserId = event.cookies.get('cactoideUserId'); const cactoideUserId = event.cookies.get('cactoideUserId');
const userId = generateUserId();
const DAYS = 400; // practical upper bound in many browsers for cookies const DAYS = 400; // practical upper bound in many browsers for cookies
const MAX_AGE = 60 * 60 * 24 * DAYS; const MAX_AGE = 60 * 60 * 24 * DAYS;
const PATH = '/'; const PATH = '/';
if (!cactoideUserId) { if (!cactoideUserId) {
logger.debug({ userId }, 'No cactoideUserId cookie found, generating new one'); logger.debug('No cactoideUserId cookie found, generating new one');
event.cookies.set('cactoideUserId', userId, { path: PATH, maxAge: MAX_AGE }); event.cookies.set('cactoideUserId', generateUserId(), { path: PATH, maxAge: MAX_AGE });
} else { } else {
logger.debug({ cactoideUserId }, 'cactoideUserId cookie found, using existing one'); logger.debug('cactoideUserId cookie found, using existing one');
} }
return resolve(event); return resolve(event);

View File

@@ -1,5 +1,5 @@
export const generateUserId = () => { import { randomUUID } from 'crypto';
const userId = 'user_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
return userId; // This id is the only credential the app has — it must not be guessable,
}; // and it must never be serialized to the client.
export const generateUserId = () => 'user_' + randomUUID();

View File

@@ -259,8 +259,6 @@
"layout": { "layout": {
"defaultTitle": "Cactoide -", "defaultTitle": "Cactoide -",
"defaultDescription": "Crea e gestisci gli RSVP degli eventi", "defaultDescription": "Crea e gestisci gli RSVP degli eventi",
"userIdCookieText": "Il tuo UserID memorizzato come cookie:",
"firstTimeVisiting": "Prima visita. Generazione di un nuovo UserID...",
"copyright": "© 2025 Cactoide" "copyright": "© 2025 Cactoide"
} }
} }

View File

@@ -291,8 +291,6 @@
"layout": { "layout": {
"defaultTitle": "Cactoide -", "defaultTitle": "Cactoide -",
"defaultDescription": "Create and manage event RSVPs", "defaultDescription": "Create and manage event RSVPs",
"userIdCookieText": "Your UserID stored as a cookie:",
"firstTimeVisiting": "First time visiting. Generating new UserID...",
"copyright": "© 2025 Cactoide" "copyright": "© 2025 Cactoide"
} }
} }

View File

@@ -14,7 +14,7 @@ export interface Event {
type: EventType; type: EventType;
attendee_limit?: number; attendee_limit?: number;
visibility: EventVisibility; visibility: EventVisibility;
user_id: string; is_creator?: boolean; // Optional: absent on events fetched from federated instances
created_at: string; created_at: string;
updated_at: string; updated_at: string;
federation?: boolean; // Optional: true if event is from a federated instance federation?: boolean; // Optional: true if event is from a federated instance
@@ -25,7 +25,7 @@ export interface RSVP {
id: string; id: string;
event_id: string; event_id: string;
name: string; name: string;
user_id: string; is_mine: boolean;
created_at: string; created_at: string;
} }

View File

@@ -1,7 +0,0 @@
export function load({ cookies }) {
const cactoideUserId = cookies.get('cactoideUserId');
return {
cactoideUserId
};
}

View File

@@ -3,7 +3,7 @@
import Navbar from '$lib/components/Navbar.svelte'; import Navbar from '$lib/components/Navbar.svelte';
import { t } from '$lib/i18n/i18n.js'; import { t } from '$lib/i18n/i18n.js';
let { data, children } = $props(); let { children } = $props();
</script> </script>
<svelte:head> <svelte:head>
@@ -28,12 +28,6 @@
<footer class="py-12"> <footer class="py-12">
<div class="container mx-auto px-4 text-center"> <div class="container mx-auto px-4 text-center">
<div class="text-sm"> <div class="text-sm">
<p class="mb-4 text-gray-100/80">
{t('layout.userIdCookieText')}
<span class="font-bold text-violet-400"
>{data.cactoideUserId ? data.cactoideUserId : t('layout.firstTimeVisiting')}</span
>
</p>
<p>{t('layout.copyright')}</p> <p>{t('layout.copyright')}</p>
</div> </div>
</div> </div>

View File

@@ -33,7 +33,6 @@ export const GET: RequestHandler = async () => {
federation: true, federation: true,
attendee_limit: event.attendeeLimit, attendee_limit: event.attendeeLimit,
visibility: event.visibility, visibility: event.visibility,
user_id: event.userId,
created_at: event.createdAt?.toISOString() || '', created_at: event.createdAt?.toISOString() || '',
updated_at: event.updatedAt?.toISOString() || '' updated_at: event.updatedAt?.toISOString() || ''
})); }));

View File

@@ -20,7 +20,6 @@
let errors: Record<string, string> = {}; let errors: Record<string, string> = {};
let isSubmitting = false; let isSubmitting = false;
let currentUserId = '';
// Get today's date in YYYY-MM-DD format for min attribute // Get today's date in YYYY-MM-DD format for min attribute
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];
@@ -95,7 +94,6 @@
}} }}
class="space-y-6" class="space-y-6"
> >
<input type="hidden" name="userId" value={currentUserId} />
<input type="hidden" name="type" value={eventData.type} /> <input type="hidden" name="type" value={eventData.type} />
<input type="hidden" name="visibility" value={eventData.visibility} /> <input type="hidden" name="visibility" value={eventData.visibility} />
<input type="hidden" name="location_type" value={eventData.location_type} /> <input type="hidden" name="location_type" value={eventData.location_type} />

View File

@@ -26,7 +26,6 @@ export const load: PageServerLoad = async () => {
type: event.type, type: event.type,
attendee_limit: event.attendeeLimit, attendee_limit: event.attendeeLimit,
visibility: event.visibility, visibility: event.visibility,
user_id: event.userId,
created_at: event.createdAt?.toISOString(), created_at: event.createdAt?.toISOString(),
updated_at: event.updatedAt?.toISOString(), updated_at: event.updatedAt?.toISOString(),
federation: false // Add false for local events federation: false // Add false for local events

View File

@@ -30,7 +30,6 @@ export const load = async ({ cookies }) => {
type: event.type, type: event.type,
attendee_limit: event.attendeeLimit, attendee_limit: event.attendeeLimit,
visibility: event.visibility, visibility: event.visibility,
user_id: event.userId,
created_at: event.createdAt?.toISOString() || new Date().toISOString(), created_at: event.createdAt?.toISOString() || new Date().toISOString(),
updated_at: event.updatedAt?.toISOString() || new Date().toISOString() updated_at: event.updatedAt?.toISOString() || new Date().toISOString()
})); }));

View File

@@ -7,7 +7,6 @@
export let data: { events: Event[] }; export let data: { events: Event[] };
let userEvents: Event[] = []; let userEvents: Event[] = [];
let currentUserId = '';
let showDeleteModal = false; let showDeleteModal = false;
let eventToDelete: Event | null = null; let eventToDelete: Event | null = null;
@@ -28,7 +27,6 @@
// Use server-side action for deletion // Use server-side action for deletion
const formData = new FormData(); const formData = new FormData();
formData.append('eventId', eventId); formData.append('eventId', eventId);
formData.append('userId', currentUserId);
const response = await fetch('?/deleteEvent', { const response = await fetch('?/deleteEvent', {
method: 'POST', method: 'POST',

View File

@@ -7,6 +7,7 @@ import { logger } from '$lib/logger';
export const load: PageServerLoad = async ({ params, cookies }) => { export const load: PageServerLoad = async ({ params, cookies }) => {
const eventId = params.id; const eventId = params.id;
const userId = cookies.get('cactoideUserId');
if (!eventId) { if (!eventId) {
throw error(404, 'EventId not found'); throw error(404, 'EventId not found');
@@ -29,7 +30,6 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
// Check if this is an invite-only event // Check if this is an invite-only event
if (event.visibility === 'invite-only') { if (event.visibility === 'invite-only') {
// For invite-only events, check if user is the event creator // For invite-only events, check if user is the event creator
const userId = cookies.get('cactoideUserId');
if (event.userId !== userId) { if (event.userId !== userId) {
// User is not the creator, redirect to a message about needing invite // User is not the creator, redirect to a message about needing invite
throw error(403, 'This event requires an invite link to view'); throw error(403, 'This event requires an invite link to view');
@@ -48,7 +48,8 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
type: event.type, type: event.type,
attendee_limit: event.attendeeLimit, attendee_limit: event.attendeeLimit,
visibility: event.visibility, visibility: event.visibility,
user_id: event.userId, // Never send raw user ids to the client — they are the credential
is_creator: !!userId && event.userId === userId,
created_at: event.createdAt?.toISOString() || new Date().toISOString(), created_at: event.createdAt?.toISOString() || new Date().toISOString(),
updated_at: event.updatedAt?.toISOString() || new Date().toISOString() updated_at: event.updatedAt?.toISOString() || new Date().toISOString()
}; };
@@ -57,16 +58,13 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
id: rsvp.id, id: rsvp.id,
event_id: rsvp.eventId, event_id: rsvp.eventId,
name: rsvp.name, name: rsvp.name,
user_id: rsvp.userId, is_mine: !!userId && rsvp.userId === userId,
created_at: rsvp.createdAt?.toISOString() || new Date().toISOString() created_at: rsvp.createdAt?.toISOString() || new Date().toISOString()
})); }));
const userId = cookies.get('cactoideUserId');
return { return {
event: transformedEvent, event: transformedEvent,
rsvps: transformedRsvps, rsvps: transformedRsvps
userId: userId
}; };
} catch (err) { } catch (err) {
if (err instanceof Response) throw err; // This is the 404 error if (err instanceof Response) throw err; // This is the 404 error

View File

@@ -9,7 +9,7 @@
import type { CalendarEvent } from '$lib/calendarHelpers.js'; import type { CalendarEvent } from '$lib/calendarHelpers.js';
import { t } from '$lib/i18n/i18n.js'; import { t } from '$lib/i18n/i18n.js';
export let data: { event: Event; rsvps: RSVP[]; userId: string }; export let data: { event: Event; rsvps: RSVP[] };
type FormDataLocal = { success?: boolean; error?: string; type?: 'add' | 'remove' | 'copy' }; type FormDataLocal = { success?: boolean; error?: string; type?: 'add' | 'remove' | 'copy' };
export let form: FormDataLocal | undefined; export let form: FormDataLocal | undefined;
@@ -30,8 +30,7 @@
// Use server-side data // Use server-side data
$: event = data.event; $: event = data.event;
$: rsvps = data.rsvps; $: rsvps = data.rsvps;
$: currentUserId = data.userId; $: isEventCreator = event.is_creator ?? false;
$: isEventCreator = event.user_id === currentUserId;
// Create calendar event object when event data changes // Create calendar event object when event data changes
$: if (event && browser) { $: if (event && browser) {
@@ -266,7 +265,6 @@
}} }}
class="space-y-4" class="space-y-4"
> >
<input type="hidden" name="userId" value={currentUserId} />
<div> <div>
<label for="attendeeName" class=" mb-2 block text-sm font-semibold"> <label for="attendeeName" class=" mb-2 block text-sm font-semibold">
{t('event.yourNameLabel')} {t('event.yourNameLabel')}
@@ -401,7 +399,7 @@
</div> </div>
</div> </div>
{#if attendee.user_id === currentUserId} {#if attendee.is_mine}
<form <form
method="POST" method="POST"
action="?/removeRSVP" action="?/removeRSVP"

View File

@@ -44,10 +44,13 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
} }
} }
// Strip the owner id — it is the credential, and the load above already proved ownership
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { userId: _owner, ...eventRow } = event[0];
return { return {
event: event[0], event: eventRow,
inviteToken, inviteToken
userId
}; };
}; };

View File

@@ -394,7 +394,7 @@
</div> </div>
<!-- Invite Link Section (only for invite-only events and event creator) --> <!-- Invite Link Section (only for invite-only events and event creator) -->
{#if eventData.visibility === 'invite-only' && inviteToken && data.event.userId === data.userId} {#if eventData.visibility === 'invite-only' && inviteToken}
<div class="rounded-sm border border-amber-500/30 bg-amber-900/20 p-4"> <div class="rounded-sm border border-amber-500/30 bg-amber-900/20 p-4">
<div class="mb-3 flex items-center justify-between"> <div class="mb-3 flex items-center justify-between">
<h3 class="text-lg font-semibold text-amber-400">Invite Link</h3> <h3 class="text-lg font-semibold text-amber-400">Invite Link</h3>

View File

@@ -8,6 +8,7 @@ import { isTokenValid } from '$lib/inviteTokenHelpers.js';
export const load: PageServerLoad = async ({ params, cookies }) => { export const load: PageServerLoad = async ({ params, cookies }) => {
const eventId = params.id; const eventId = params.id;
const token = params.token; const token = params.token;
const userId = cookies.get('cactoideUserId');
if (!eventId || !token) { if (!eventId || !token) {
throw error(404, 'Event or token not found'); throw error(404, 'Event or token not found');
@@ -59,7 +60,8 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
type: event.type, type: event.type,
attendee_limit: event.attendeeLimit, attendee_limit: event.attendeeLimit,
visibility: event.visibility, visibility: event.visibility,
user_id: event.userId, // Never send raw user ids to the client — they are the credential
is_creator: !!userId && event.userId === userId,
created_at: event.createdAt?.toISOString() || new Date().toISOString(), created_at: event.createdAt?.toISOString() || new Date().toISOString(),
updated_at: event.updatedAt?.toISOString() || new Date().toISOString() updated_at: event.updatedAt?.toISOString() || new Date().toISOString()
}; };
@@ -68,16 +70,13 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
id: rsvp.id, id: rsvp.id,
event_id: rsvp.eventId, event_id: rsvp.eventId,
name: rsvp.name, name: rsvp.name,
user_id: rsvp.userId, is_mine: !!userId && rsvp.userId === userId,
created_at: rsvp.createdAt?.toISOString() || new Date().toISOString() created_at: rsvp.createdAt?.toISOString() || new Date().toISOString()
})); }));
const userId = cookies.get('cactoideUserId');
return { return {
event: transformedEvent, event: transformedEvent,
rsvps: transformedRsvps, rsvps: transformedRsvps,
userId: userId,
inviteToken: { inviteToken: {
id: inviteToken.id, id: inviteToken.id,
event_id: inviteToken.eventId, event_id: inviteToken.eventId,

View File

@@ -9,7 +9,7 @@
import type { CalendarEvent } from '$lib/calendarHelpers.js'; import type { CalendarEvent } from '$lib/calendarHelpers.js';
import { t } from '$lib/i18n/i18n.js'; import { t } from '$lib/i18n/i18n.js';
export let data: { event: Event; rsvps: RSVP[]; userId: string; inviteToken: InviteToken }; export let data: { event: Event; rsvps: RSVP[]; inviteToken: InviteToken };
export let form; export let form;
let event: Event; let event: Event;
@@ -26,8 +26,7 @@
// Use server-side data // Use server-side data
$: event = data.event; $: event = data.event;
$: rsvps = data.rsvps; $: rsvps = data.rsvps;
$: currentUserId = data.userId; $: isEventCreator = event.is_creator ?? false;
$: isEventCreator = event.user_id === currentUserId;
// Create calendar event object when event data changes // Create calendar event object when event data changes
$: if (event && browser) { $: if (event && browser) {
@@ -240,7 +239,6 @@
}} }}
class="space-y-4" class="space-y-4"
> >
<input type="hidden" name="userId" value={currentUserId} />
<div> <div>
<label for="attendeeName" class=" mb-2 block text-sm font-semibold"> <label for="attendeeName" class=" mb-2 block text-sm font-semibold">
{t('event.yourNameLabel')} {t('event.yourNameLabel')}
@@ -374,7 +372,7 @@
</div> </div>
</div> </div>
{#if attendee.user_id === currentUserId} {#if attendee.is_mine}
<form <form
method="POST" method="POST"
action="?/removeRSVP" action="?/removeRSVP"

View File

@@ -14,10 +14,7 @@ const config = {
// see "split" mode in https://github.com/sveltejs/kit/tree/main/packages/adapter-netlify // see "split" mode in https://github.com/sveltejs/kit/tree/main/packages/adapter-netlify
edge: false, edge: false,
split: false split: false
}), })
csrf: {
checkOrigin: false
}
} }
}; };