Compare commits

...

2 Commits

Author SHA1 Message Date
Levente Orban
fdc04502f9 feat: change the userId beahviour 2026-08-11 09:02:40 +02:00
Levente Orban
d18afc43da fix: add permissions to github workflow 2025-12-08 09:41:43 +01: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 userId = generateUserId();
const DAYS = 400; // practical upper bound in many browsers for cookies
const MAX_AGE = 60 * 60 * 24 * DAYS;
const PATH = '/';
if (!cactoideUserId) {
logger.debug({ userId }, 'No cactoideUserId cookie found, generating new one');
event.cookies.set('cactoideUserId', userId, { path: PATH, maxAge: MAX_AGE });
logger.debug('No cactoideUserId cookie found, generating new one');
event.cookies.set('cactoideUserId', generateUserId(), { path: PATH, maxAge: MAX_AGE });
} else {
logger.debug({ cactoideUserId }, 'cactoideUserId cookie found, using existing one');
logger.debug('cactoideUserId cookie found, using existing one');
}
return resolve(event);

View File

@@ -1,5 +1,5 @@
export const generateUserId = () => {
const userId = 'user_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
import { randomUUID } from 'crypto';
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": {
"defaultTitle": "Cactoide -",
"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"
}
}

View File

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

View File

@@ -14,7 +14,7 @@ export interface Event {
type: EventType;
attendee_limit?: number;
visibility: EventVisibility;
user_id: string;
is_creator?: boolean; // Optional: absent on events fetched from federated instances
created_at: string;
updated_at: string;
federation?: boolean; // Optional: true if event is from a federated instance
@@ -25,7 +25,7 @@ export interface RSVP {
id: string;
event_id: string;
name: string;
user_id: string;
is_mine: boolean;
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 { t } from '$lib/i18n/i18n.js';
let { data, children } = $props();
let { children } = $props();
</script>
<svelte:head>
@@ -28,12 +28,6 @@
<footer class="py-12">
<div class="container mx-auto px-4 text-center">
<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>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import { logger } from '$lib/logger';
export const load: PageServerLoad = async ({ params, cookies }) => {
const eventId = params.id;
const userId = cookies.get('cactoideUserId');
if (!eventId) {
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
if (event.visibility === 'invite-only') {
// For invite-only events, check if user is the event creator
const userId = cookies.get('cactoideUserId');
if (event.userId !== userId) {
// User is not the creator, redirect to a message about needing invite
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,
attendee_limit: event.attendeeLimit,
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(),
updated_at: event.updatedAt?.toISOString() || new Date().toISOString()
};
@@ -57,16 +58,13 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
id: rsvp.id,
event_id: rsvp.eventId,
name: rsvp.name,
user_id: rsvp.userId,
is_mine: !!userId && rsvp.userId === userId,
created_at: rsvp.createdAt?.toISOString() || new Date().toISOString()
}));
const userId = cookies.get('cactoideUserId');
return {
event: transformedEvent,
rsvps: transformedRsvps,
userId: userId
rsvps: transformedRsvps
};
} catch (err) {
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 { 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' };
export let form: FormDataLocal | undefined;
@@ -30,8 +30,7 @@
// Use server-side data
$: event = data.event;
$: rsvps = data.rsvps;
$: currentUserId = data.userId;
$: isEventCreator = event.user_id === currentUserId;
$: isEventCreator = event.is_creator ?? false;
// Create calendar event object when event data changes
$: if (event && browser) {
@@ -266,7 +265,6 @@
}}
class="space-y-4"
>
<input type="hidden" name="userId" value={currentUserId} />
<div>
<label for="attendeeName" class=" mb-2 block text-sm font-semibold">
{t('event.yourNameLabel')}
@@ -401,7 +399,7 @@
</div>
</div>
{#if attendee.user_id === currentUserId}
{#if attendee.is_mine}
<form
method="POST"
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 {
event: event[0],
inviteToken,
userId
event: eventRow,
inviteToken
};
};

View File

@@ -394,7 +394,7 @@
</div>
<!-- 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="mb-3 flex items-center justify-between">
<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 }) => {
const eventId = params.id;
const token = params.token;
const userId = cookies.get('cactoideUserId');
if (!eventId || !token) {
throw error(404, 'Event or token not found');
@@ -59,7 +60,8 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
type: event.type,
attendee_limit: event.attendeeLimit,
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(),
updated_at: event.updatedAt?.toISOString() || new Date().toISOString()
};
@@ -68,16 +70,13 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
id: rsvp.id,
event_id: rsvp.eventId,
name: rsvp.name,
user_id: rsvp.userId,
is_mine: !!userId && rsvp.userId === userId,
created_at: rsvp.createdAt?.toISOString() || new Date().toISOString()
}));
const userId = cookies.get('cactoideUserId');
return {
event: transformedEvent,
rsvps: transformedRsvps,
userId: userId,
inviteToken: {
id: inviteToken.id,
event_id: inviteToken.eventId,

View File

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

View File

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