Skip to content

Commit 92bc716

Browse files
authored
chore: add session auth to event-board example (#167)
- in-memory API: login/logout/me, HttpOnly `session` cookie, per-user RSVP toggle, `author` on created events - injectable `ApiService$`/`EventsService$`/`UserService$`/`IntlService$`; SSR requests forward the session cookie and locale - login page, user zone in the header, route guard with SSR 302, localized 404 page - svelte variant gets intl parity and is renamed to `svelte-nano_kit-intl-ssr` - examples sync merges shared dirs instead of replacing them, keeping variant-local files - `.env.example` per variant; API spec + auth integration tests (107 total) - vite is deduped to a single 8.1.2 across the workspace
1 parent d8ec81e commit 92bc716

126 files changed

Lines changed: 4942 additions & 1942 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/event-board/common/api/events.js

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ const events = [
1919
startsAt: new Date('2026-05-12T18:00:00Z').getTime(),
2020
location: 'Online',
2121
category: 'workshop',
22-
attendees: 24
22+
guests: 24,
23+
attendeeIds: []
2324
},
2425
{
2526
id: '2',
@@ -29,7 +30,8 @@ const events = [
2930
startsAt: new Date('2026-05-21T19:30:00Z').getTime(),
3031
location: 'Berlin',
3132
category: 'meetup',
32-
attendees: 58
33+
guests: 58,
34+
attendeeIds: []
3335
},
3436
{
3537
id: '3',
@@ -39,7 +41,8 @@ const events = [
3941
startsAt: new Date('2026-06-03T17:00:00Z').getTime(),
4042
location: 'Online',
4143
category: 'webinar',
42-
attendees: 102
44+
guests: 102,
45+
attendeeIds: []
4346
},
4447
{
4548
id: '4',
@@ -49,7 +52,8 @@ const events = [
4952
startsAt: new Date('2026-06-11T18:30:00Z').getTime(),
5053
location: 'Prague',
5154
category: 'meetup',
52-
attendees: 41
55+
guests: 41,
56+
attendeeIds: []
5357
},
5458
{
5559
id: '5',
@@ -59,7 +63,8 @@ const events = [
5963
startsAt: new Date('2026-06-24T09:00:00Z').getTime(),
6064
location: 'Amsterdam',
6165
category: 'conference',
62-
attendees: 180
66+
guests: 180,
67+
attendeeIds: []
6368
},
6469
{
6570
id: '6',
@@ -69,7 +74,8 @@ const events = [
6974
startsAt: new Date('2026-07-02T16:00:00Z').getTime(),
7075
location: 'Online',
7176
category: 'webinar',
72-
attendees: 76
77+
guests: 76,
78+
attendeeIds: []
7379
}
7480
]
7581

@@ -116,6 +122,26 @@ function sortEvents(items) {
116122
return [...items].sort((a, b) => a.startsAt - b.startsAt || a.id.localeCompare(b.id))
117123
}
118124

125+
/**
126+
* Convert an internal event record into an API payload for a user.
127+
* @param {object} event - Internal event record.
128+
* @param {string | undefined} userId - Current user id, if authenticated.
129+
* @returns {object} Event payload with `attendees` count and personal `going` flag.
130+
*/
131+
function serializeEvent(event, userId) {
132+
const {
133+
guests,
134+
attendeeIds,
135+
...publicEvent
136+
} = event
137+
138+
return {
139+
...publicEvent,
140+
attendees: guests + attendeeIds.length,
141+
going: userId ? attendeeIds.includes(userId) : false
142+
}
143+
}
144+
119145
function validateEventInput(input) {
120146
const errors = {}
121147

@@ -155,9 +181,10 @@ function validateEventInput(input) {
155181
* @param {string | undefined} query.category - Category filter.
156182
* @param {string | undefined} query.cursor - Anchor cursor based on `startsAt`.
157183
* @param {string | undefined} query.limit - Page size.
184+
* @param {string | undefined} userId - Current user id, if authenticated.
158185
* @returns {{ status: number, body: { events: object[], nextCursor?: number } | { error: string } }} API response payload.
159186
*/
160-
export function listEvents(query) {
187+
export function listEvents(query, userId) {
161188
const q = query.q?.trim().toLowerCase()
162189
const category = normalizeCategory(query.category)
163190
const cursor = Number(query.cursor || 0)
@@ -193,7 +220,7 @@ export function listEvents(query) {
193220
return {
194221
status: 200,
195222
body: {
196-
events: pageItems,
223+
events: pageItems.map(event => serializeEvent(event, userId)),
197224
nextCursor: hasMore ? pageItems[pageItems.length - 1]?.startsAt : undefined
198225
}
199226
}
@@ -202,18 +229,22 @@ export function listEvents(query) {
202229
/**
203230
* Find an event by slug.
204231
* @param {string} slug - Event slug.
205-
* @returns {object | null} Event object or `null` when it does not exist.
232+
* @param {string | undefined} userId - Current user id, if authenticated.
233+
* @returns {object | null} Event payload or `null` when it does not exist.
206234
*/
207-
export function findEvent(slug) {
208-
return events.find(event => event.slug === slug) || null
235+
export function findEvent(slug, userId) {
236+
const event = events.find(item => item.slug === slug) || null
237+
238+
return event && serializeEvent(event, userId)
209239
}
210240

211241
/**
212242
* Create a new event in the in-memory store.
213243
* @param {object} input - Event form payload.
244+
* @param {object | null} user - Current user, if authenticated.
214245
* @returns {{ status: number, body: object }} API response payload.
215246
*/
216-
export function createEvent(input) {
247+
export function createEvent(input, user) {
217248
const errors = validateEventInput(input)
218249

219250
if (Object.keys(errors).length > 0) {
@@ -233,30 +264,42 @@ export function createEvent(input) {
233264
startsAt: input.startsAt,
234265
location: input.location.trim(),
235266
category: input.category,
236-
attendees: 0
267+
author: user?.name,
268+
guests: 0,
269+
attendeeIds: []
237270
}
238271

239272
events.push(event)
240273

241274
return {
242275
status: 201,
243-
body: event
276+
body: serializeEvent(event, user?.id)
244277
}
245278
}
246279

247280
/**
248-
* Increment attendees count for an event.
281+
* Register an RSVP for an event. An authenticated user toggles their personal
282+
* attendance, an anonymous request just increments the guest counter.
249283
* @param {string} id - Event id.
250-
* @returns {object | null} Updated event object or `null` when it does not exist.
284+
* @param {string | undefined} userId - Current user id, if authenticated.
285+
* @returns {object | null} Updated event payload or `null` when it does not exist.
251286
*/
252-
export function rsvpEvent(id) {
287+
export function rsvpEvent(id, userId) {
253288
const event = events.find(item => item.id === id) || null
254289

255290
if (!event) {
256291
return null
257292
}
258293

259-
event.attendees += 1
294+
if (userId) {
295+
if (event.attendeeIds.includes(userId)) {
296+
event.attendeeIds = event.attendeeIds.filter(attendeeId => attendeeId !== userId)
297+
} else {
298+
event.attendeeIds = [...event.attendeeIds, userId]
299+
}
300+
} else {
301+
event.guests += 1
302+
}
260303

261-
return event
304+
return serializeEvent(event, userId)
262305
}

examples/event-board/common/api/index.js

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,113 @@
11
import { Hono } from 'hono'
2+
import {
3+
deleteCookie,
4+
getCookie,
5+
setCookie
6+
} from 'hono/cookie'
27
import {
38
createEvent,
49
findEvent,
510
listEvents,
611
rsvpEvent
712
} from './events.js'
13+
import {
14+
authenticateUser,
15+
createSession,
16+
deleteSession,
17+
findUserBySession,
18+
publicUser
19+
} from './users.js'
820

921
const HTTP_BAD_REQUEST = 400
22+
const HTTP_UNAUTHORIZED = 401
1023
const HTTP_NOT_FOUND = 404
24+
const SESSION_COOKIE = 'session'
25+
const SESSION_MAX_AGE = 60 * 60 * 24 * 30
26+
27+
/**
28+
* Resolve the current user from the request session cookie.
29+
* @param {import('hono').Context} c
30+
* @returns {object | null} Session user or `null`.
31+
*/
32+
function currentUser(c) {
33+
return findUserBySession(getCookie(c, SESSION_COOKIE))
34+
}
1135

1236
export function api() {
1337
const app = new Hono()
1438

39+
app.post('/api/auth/login', async (c) => {
40+
let body
41+
42+
try {
43+
body = await c.req.json()
44+
} catch {
45+
return c.json({
46+
error: 'Expected JSON payload'
47+
}, HTTP_BAD_REQUEST)
48+
}
49+
50+
const user = authenticateUser(body?.username, body?.password)
51+
52+
if (!user) {
53+
return c.json({
54+
error: 'Invalid username or password'
55+
}, HTTP_UNAUTHORIZED)
56+
}
57+
58+
setCookie(c, SESSION_COOKIE, createSession(user.id), {
59+
path: '/',
60+
httpOnly: true,
61+
sameSite: 'Lax',
62+
maxAge: SESSION_MAX_AGE
63+
})
64+
65+
return c.json(publicUser(user))
66+
})
67+
68+
app.post('/api/auth/logout', (c) => {
69+
const token = getCookie(c, SESSION_COOKIE)
70+
71+
if (token) {
72+
deleteSession(token)
73+
}
74+
75+
deleteCookie(c, SESSION_COOKIE, {
76+
path: '/'
77+
})
78+
79+
return c.json({
80+
ok: true
81+
})
82+
})
83+
84+
app.get('/api/users/me', (c) => {
85+
const user = currentUser(c)
86+
87+
if (!user) {
88+
return c.json({
89+
error: 'Unauthenticated'
90+
}, HTTP_UNAUTHORIZED)
91+
}
92+
93+
return c.json(publicUser(user))
94+
})
95+
1596
app.get('/api/events', (c) => {
97+
const user = currentUser(c)
1698
const result = listEvents({
1799
q: c.req.query('q'),
18100
category: c.req.query('category'),
19101
cursor: c.req.query('cursor'),
20102
limit: c.req.query('limit')
21-
})
103+
}, user?.id)
22104

23105
return c.json(result.body, result.status)
24106
})
25107

26108
app.get('/api/events/:slug', (c) => {
27-
const event = findEvent(c.req.param('slug'))
109+
const user = currentUser(c)
110+
const event = findEvent(c.req.param('slug'), user?.id)
28111

29112
if (!event) {
30113
return c.json(null, HTTP_NOT_FOUND)
@@ -34,6 +117,14 @@ export function api() {
34117
})
35118

36119
app.post('/api/events', async (c) => {
120+
const user = currentUser(c)
121+
122+
if (!user) {
123+
return c.json({
124+
error: 'Unauthenticated'
125+
}, HTTP_UNAUTHORIZED)
126+
}
127+
37128
let body
38129

39130
try {
@@ -46,13 +137,14 @@ export function api() {
46137
}, HTTP_BAD_REQUEST)
47138
}
48139

49-
const result = createEvent(body)
140+
const result = createEvent(body, user)
50141

51142
return c.json(result.body, result.status)
52143
})
53144

54145
app.post('/api/events/:id/rsvp', (c) => {
55-
const event = rsvpEvent(c.req.param('id'))
146+
const user = currentUser(c)
147+
const event = rsvpEvent(c.req.param('id'), user?.id)
56148

57149
if (!event) {
58150
return c.json(null, HTTP_NOT_FOUND)

0 commit comments

Comments
 (0)