a mini social media app for small communities
1module main
2
3import veb
4import auth
5import entity { Site, User, Post, Like, LikeCache }
6
7////// Users //////
8
9@['/api/user/register'; post]
10fn (mut app App) api_user_register(mut ctx Context, username string, password string) veb.Result {
11 if app.get_user_by_name(username) != none {
12 ctx.error('username taken')
13 return ctx.redirect('/register')
14 }
15
16 // validate username
17 if !app.validators.username.validate(username) {
18 ctx.error('invalid username')
19 return ctx.redirect('/register')
20 }
21
22 // validate password
23 if !app.validators.password.validate(password) {
24 ctx.error('invalid password')
25 return ctx.redirect('/register')
26 }
27
28 salt := auth.generate_salt()
29 mut user := User{
30 username: username
31 password: auth.hash_password_with_salt(password, salt)
32 password_salt: salt
33 }
34
35 if app.config.instance.default_theme != '' {
36 user.theme = app.config.instance.default_theme
37 }
38
39 sql app.db {
40 insert user into User
41 } or {
42 eprintln('failed to insert user ${user}')
43 return ctx.redirect('/')
44 }
45
46 println('reg: ${username}')
47
48 if x := app.get_user_by_name(username) {
49 token := app.auth.add_token(x.id, ctx.ip()) or {
50 eprintln(err)
51 ctx.error('could not create token for user with id ${user.id}')
52 return ctx.redirect('/')
53 }
54 ctx.set_cookie(
55 name: 'token'
56 value: token
57 same_site: .same_site_none_mode
58 secure: true
59 path: '/'
60 )
61 } else {
62 eprintln('could not log into newly-created user: ${user}')
63 ctx.error('could not log into newly-created user.')
64 }
65
66 return ctx.redirect('/')
67}
68
69@['/api/user/login'; post]
70fn (mut app App) api_user_login(mut ctx Context, username string, password string) veb.Result {
71 user := app.get_user_by_name(username) or {
72 ctx.error('invalid credentials')
73 return ctx.redirect('/login')
74 }
75
76 if !auth.compare_password_with_hash(password, user.password_salt, user.password) {
77 ctx.error('invalid credentials')
78 return ctx.redirect('/login')
79 }
80
81 token := app.auth.add_token(user.id, ctx.ip()) or {
82 eprintln('failed to add token on log in: ${err}')
83 ctx.error('could not create token for user with id ${user.id}')
84 return ctx.redirect('/login')
85 }
86
87 ctx.set_cookie(
88 name: 'token'
89 value: token
90 same_site: .same_site_none_mode
91 secure: true
92 path: '/'
93 )
94
95 return ctx.redirect('/')
96}
97
98@['/api/user/logout']
99fn (mut app App) api_user_logout(mut ctx Context) veb.Result {
100 if token := ctx.get_cookie('token') {
101 if user := app.get_user_by_token(ctx, token) {
102 app.auth.delete_tokens_for_ip(ctx.ip()) or {
103 eprintln('failed to yeet tokens for ${user.id} with ip ${ctx.ip()}')
104 return ctx.redirect('/login')
105 }
106 } else {
107 eprintln('failed to get user for token for logout')
108 }
109 } else {
110 eprintln('failed to get token cookie for logout')
111 }
112
113 ctx.set_cookie(
114 name: 'token'
115 value: ''
116 same_site: .same_site_none_mode
117 secure: true
118 path: '/'
119 )
120
121 return ctx.redirect('/login')
122}
123
124@['/api/user/full_logout']
125fn (mut app App) api_user_full_logout(mut ctx Context) veb.Result {
126 if token := ctx.get_cookie('token') {
127 if user := app.get_user_by_token(ctx, token) {
128 app.auth.delete_tokens_for_user(user.id) or {
129 eprintln('failed to yeet tokens for ${user.id}')
130 return ctx.redirect('/login')
131 }
132 } else {
133 eprintln('failed to get user for token for full_logout')
134 }
135 } else {
136 eprintln('failed to get token cookie for full_logout')
137 }
138
139 ctx.set_cookie(
140 name: 'token'
141 value: ''
142 same_site: .same_site_none_mode
143 secure: true
144 path: '/'
145 )
146
147 return ctx.redirect('/login')
148}
149
150@['/api/user/set_nickname'; post]
151fn (mut app App) api_user_set_nickname(mut ctx Context, nickname string) veb.Result {
152 user := app.whoami(mut ctx) or {
153 ctx.error('you are not logged in!')
154 return ctx.redirect('/login')
155 }
156
157 mut clean_nickname := ?string(nickname.trim_space())
158 if clean_nickname or { '' } == '' {
159 clean_nickname = none
160 }
161
162 // validate
163 if clean_nickname != none && !app.validators.nickname.validate(clean_nickname or { '' }) {
164 ctx.error('invalid nickname')
165 return ctx.redirect('/me')
166 }
167
168 sql app.db {
169 update User set nickname = clean_nickname where id == user.id
170 } or {
171 ctx.error('failed to change nickname')
172 eprintln('failed to update nickname for ${user} (${user.nickname} -> ${clean_nickname})')
173 return ctx.redirect('/me')
174 }
175
176 return ctx.redirect('/me')
177}
178
179@['/api/user/set_muted'; post]
180fn (mut app App) api_user_set_muted(mut ctx Context, muted bool) veb.Result {
181 user := app.whoami(mut ctx) or {
182 ctx.error('you are not logged in!')
183 return ctx.redirect('/login')
184 }
185
186 if user.admin || app.config.dev_mode {
187 sql app.db {
188 update User set muted = muted where id == user.id
189 } or {
190 ctx.error('failed to change mute status')
191 eprintln('failed to update mute status for ${user} (${user.muted} -> ${muted})')
192 return ctx.redirect('/user/${user.username}')
193 }
194 return ctx.redirect('/user/${user.username}')
195 } else {
196 ctx.error('insufficient permissions!')
197 eprintln('insufficient perms to update mute status for ${user} (${user.muted} -> ${muted})')
198 return ctx.redirect('/user/${user.username}')
199 }
200}
201
202@['/api/user/set_theme'; post]
203fn (mut app App) api_user_set_theme(mut ctx Context, url string) veb.Result {
204 if !app.config.instance.allow_changing_theme {
205 ctx.error('this instance disallows changing themes :(')
206 return ctx.redirect('/me')
207 }
208
209 user := app.whoami(mut ctx) or {
210 ctx.error('you are not logged in!')
211 return ctx.redirect('/login')
212 }
213
214 mut theme := ?string(none)
215 if url.trim_space() != '' {
216 theme = url.trim_space()
217 }
218
219 sql app.db {
220 update User set theme = theme where id == user.id
221 } or {
222 ctx.error('failed to change theme')
223 eprintln('failed to update theme for ${user} (${user.theme} -> ${theme})')
224 return ctx.redirect('/me')
225 }
226
227 return ctx.redirect('/me')
228}
229
230@['/api/user/set_pronouns'; post]
231fn (mut app App) api_user_set_pronouns(mut ctx Context, pronouns string) veb.Result {
232 user := app.whoami(mut ctx) or {
233 ctx.error('you are not logged in!')
234 return ctx.redirect('/login')
235 }
236
237 clean_pronouns := pronouns.trim_space()
238 if !app.validators.pronouns.validate(clean_pronouns) {
239 ctx.error('invalid pronouns')
240 return ctx.redirect('/me')
241 }
242
243 sql app.db {
244 update User set pronouns = clean_pronouns where id == user.id
245 } or {
246 ctx.error('failed to change pronouns')
247 eprintln('failed to update pronouns for ${user} (${user.pronouns} -> ${clean_pronouns})')
248 return ctx.redirect('/me')
249 }
250
251 return ctx.redirect('/me')
252}
253
254@['/api/user/set_bio'; post]
255fn (mut app App) api_user_set_bio(mut ctx Context, bio string) veb.Result {
256 user := app.whoami(mut ctx) or {
257 ctx.error('you are not logged in!')
258 return ctx.redirect('/login')
259 }
260
261 clean_bio := bio.trim_space()
262 if !app.validators.user_bio.validate(clean_bio) {
263 ctx.error('invalid bio')
264 return ctx.redirect('/me')
265 }
266
267 sql app.db {
268 update User set bio = clean_bio where id == user.id
269 } or {
270 ctx.error('failed to change bio')
271 eprintln('failed to update bio for ${user} (${user.bio} -> ${clean_bio})')
272 return ctx.redirect('/me')
273 }
274
275 return ctx.redirect('/me')
276}
277
278@['/api/user/get_name']
279fn (mut app App) api_user_get_name(mut ctx Context, username string) veb.Result {
280 user := app.get_user_by_name(username) or {
281 return ctx.server_error('no such user')
282 }
283 return ctx.text(user.get_name())
284}
285
286////// Posts //////
287
288@['/api/post/new_post'; post]
289fn (mut app App) api_post_new_post(mut ctx Context, title string, body string) veb.Result {
290 user := app.whoami(mut ctx) or {
291 ctx.error('not logged in!')
292 return ctx.redirect('/')
293 }
294
295 if user.muted {
296 ctx.error('you are muted!')
297 return ctx.redirect('/me')
298 }
299
300 // validate title
301 if !app.validators.post_title.validate(title) {
302 ctx.error('invalid title')
303 return ctx.redirect('/me')
304 }
305
306 // validate body
307 if !app.validators.post_body.validate(body) {
308 ctx.error('invalid body')
309 return ctx.redirect('/me')
310 }
311
312 post := Post{
313 author_id: user.id
314 title: title
315 body: body
316 }
317
318 sql app.db {
319 insert post into Post
320 } or {
321 ctx.error('failed to post!')
322 println('failed to post: ${post} from user ${user.id}')
323 return ctx.redirect('/me')
324 }
325
326 return ctx.redirect('/me')
327}
328
329@['/api/post/delete'; post]
330fn (mut app App) api_post_delete(mut ctx Context, id int) veb.Result {
331 user := app.whoami(mut ctx) or {
332 ctx.error('not logged in!')
333 return ctx.redirect('/login')
334 }
335
336 post := app.get_post_by_id(id) or {
337 ctx.error('post does not exist')
338 return ctx.redirect('/')
339 }
340
341 if user.admin || user.id == post.author_id {
342 sql app.db {
343 delete from Post where id == id
344 delete from Like where post_id == id
345 } or {
346 ctx.error('failed to delete post')
347 eprintln('failed to delete post: ${id}')
348 return ctx.redirect('/')
349 }
350 println('deleted post: ${id}')
351 return ctx.redirect('/')
352 } else {
353 ctx.error('insufficient permissions!')
354 eprintln('insufficient perms to delete post: ${id} (${user.id})')
355 return ctx.redirect('/')
356 }
357}
358
359@['/api/post/like']
360fn (mut app App) api_post_like(mut ctx Context, id int) veb.Result {
361 user := app.whoami(mut ctx) or {
362 return ctx.unauthorized('not logged in')
363 }
364
365 post := app.get_post_by_id(id) or {
366 return ctx.server_error('post does not exist')
367 }
368
369 if app.does_user_like_post(user.id, post.id) {
370 sql app.db {
371 delete from Like where user_id == user.id && post_id == post.id
372 // yeet the old cached like value
373 delete from LikeCache where post_id == post.id
374 } or {
375 eprintln('user ${user.id} failed to unlike post ${id}')
376 return ctx.server_error('failed to unlike post')
377 }
378 return ctx.ok('unliked post')
379 } else {
380 // remove the old dislike, if it exists
381 if app.does_user_dislike_post(user.id, post.id) {
382 sql app.db {
383 delete from Like where user_id == user.id && post_id == post.id
384 } or {
385 eprintln('user ${user.id} failed to remove dislike on post ${id} when liking it')
386 }
387 }
388
389 like := Like{
390 user_id: user.id
391 post_id: post.id
392 is_like: true
393 }
394 sql app.db {
395 insert like into Like
396 // yeet the old cached like value
397 delete from LikeCache where post_id == post.id
398 } or {
399 eprintln('user ${user.id} failed to like post ${id}')
400 return ctx.server_error('failed to like post')
401 }
402 return ctx.ok('liked post')
403 }
404}
405
406@['/api/post/dislike']
407fn (mut app App) api_post_dislike(mut ctx Context, id int) veb.Result {
408 user := app.whoami(mut ctx) or {
409 return ctx.unauthorized('not logged in')
410 }
411
412 post := app.get_post_by_id(id) or {
413 return ctx.server_error('post does not exist')
414 }
415
416 if app.does_user_dislike_post(user.id, post.id) {
417 sql app.db {
418 delete from Like where user_id == user.id && post_id == post.id
419 // yeet the old cached like value
420 delete from LikeCache where post_id == post.id
421 } or {
422 eprintln('user ${user.id} failed to unlike post ${id}')
423 return ctx.server_error('failed to unlike post')
424 }
425 return ctx.ok('undisliked post')
426 } else {
427 // remove the old like, if it exists
428 if app.does_user_like_post(user.id, post.id) {
429 sql app.db {
430 delete from Like where user_id == user.id && post_id == post.id
431 } or {
432 eprintln('user ${user.id} failed to remove like on post ${id} when disliking it')
433 }
434 }
435
436 like := Like{
437 user_id: user.id
438 post_id: post.id
439 is_like: false
440 }
441 sql app.db {
442 insert like into Like
443 // yeet the old cached like value
444 delete from LikeCache where post_id == post.id
445 } or {
446 eprintln('user ${user.id} failed to dislike post ${id}')
447 return ctx.server_error('failed to dislike post')
448 }
449 return ctx.ok('disliked post')
450 }
451}
452
453////// Site //////
454
455@['/api/site/set_motd'; post]
456fn (mut app App) api_site_set_motd(mut ctx Context, motd string) veb.Result {
457 user := app.whoami(mut ctx) or {
458 ctx.error('not logged in!')
459 return ctx.redirect('/login')
460 }
461
462 if user.admin {
463 sql app.db {
464 update Site set motd = motd where id == 1
465 } or {
466 ctx.error('failed to set motd')
467 eprintln('failed to set motd: ${motd}')
468 return ctx.redirect('/')
469 }
470 println('set motd to: ${motd}')
471 return ctx.redirect('/')
472 } else {
473 ctx.error('insufficient permissions!')
474 eprintln('insufficient perms to set motd to: ${motd} (${user.id})')
475 return ctx.redirect('/')
476 }
477}