a mini social media app for small communities
1module main
2
3import veb
4import auth
5import entity { Like, LikeCache, Post, Site, User }
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 { return ctx.server_error('no such user') }
281 return ctx.text(user.get_name())
282}
283
284////// Posts //////
285
286@['/api/post/new_post'; post]
287fn (mut app App) api_post_new_post(mut ctx Context, title string, body string) veb.Result {
288 user := app.whoami(mut ctx) or {
289 ctx.error('not logged in!')
290 return ctx.redirect('/')
291 }
292
293 if user.muted {
294 ctx.error('you are muted!')
295 return ctx.redirect('/me')
296 }
297
298 // validate title
299 if !app.validators.post_title.validate(title) {
300 ctx.error('invalid title')
301 return ctx.redirect('/me')
302 }
303
304 // validate body
305 if !app.validators.post_body.validate(body) {
306 ctx.error('invalid body')
307 return ctx.redirect('/me')
308 }
309
310 post := Post{
311 author_id: user.id
312 title: title
313 body: body
314 }
315
316 sql app.db {
317 insert post into Post
318 } or {
319 ctx.error('failed to post!')
320 println('failed to post: ${post} from user ${user.id}')
321 return ctx.redirect('/me')
322 }
323
324 return ctx.redirect('/me')
325}
326
327@['/api/post/delete'; post]
328fn (mut app App) api_post_delete(mut ctx Context, id int) veb.Result {
329 user := app.whoami(mut ctx) or {
330 ctx.error('not logged in!')
331 return ctx.redirect('/login')
332 }
333
334 post := app.get_post_by_id(id) or {
335 ctx.error('post does not exist')
336 return ctx.redirect('/')
337 }
338
339 if user.admin || user.id == post.author_id {
340 sql app.db {
341 delete from Post where id == id
342 delete from Like where post_id == id
343 } or {
344 ctx.error('failed to delete post')
345 eprintln('failed to delete post: ${id}')
346 return ctx.redirect('/')
347 }
348 println('deleted post: ${id}')
349 return ctx.redirect('/')
350 } else {
351 ctx.error('insufficient permissions!')
352 eprintln('insufficient perms to delete post: ${id} (${user.id})')
353 return ctx.redirect('/')
354 }
355}
356
357@['/api/post/like']
358fn (mut app App) api_post_like(mut ctx Context, id int) veb.Result {
359 user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
360
361 post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') }
362
363 if app.does_user_like_post(user.id, post.id) {
364 sql app.db {
365 delete from Like where user_id == user.id && post_id == post.id
366 // yeet the old cached like value
367 delete from LikeCache where post_id == post.id
368 } or {
369 eprintln('user ${user.id} failed to unlike post ${id}')
370 return ctx.server_error('failed to unlike post')
371 }
372 return ctx.ok('unliked post')
373 } else {
374 // remove the old dislike, if it exists
375 if app.does_user_dislike_post(user.id, post.id) {
376 sql app.db {
377 delete from Like where user_id == user.id && post_id == post.id
378 } or {
379 eprintln('user ${user.id} failed to remove dislike on post ${id} when liking it')
380 }
381 }
382
383 like := Like{
384 user_id: user.id
385 post_id: post.id
386 is_like: true
387 }
388 sql app.db {
389 insert like into Like
390 // yeet the old cached like value
391 delete from LikeCache where post_id == post.id
392 } or {
393 eprintln('user ${user.id} failed to like post ${id}')
394 return ctx.server_error('failed to like post')
395 }
396 return ctx.ok('liked post')
397 }
398}
399
400@['/api/post/dislike']
401fn (mut app App) api_post_dislike(mut ctx Context, id int) veb.Result {
402 user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
403
404 post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') }
405
406 if app.does_user_dislike_post(user.id, post.id) {
407 sql app.db {
408 delete from Like where user_id == user.id && post_id == post.id
409 // yeet the old cached like value
410 delete from LikeCache where post_id == post.id
411 } or {
412 eprintln('user ${user.id} failed to unlike post ${id}')
413 return ctx.server_error('failed to unlike post')
414 }
415 return ctx.ok('undisliked post')
416 } else {
417 // remove the old like, if it exists
418 if app.does_user_like_post(user.id, post.id) {
419 sql app.db {
420 delete from Like where user_id == user.id && post_id == post.id
421 } or {
422 eprintln('user ${user.id} failed to remove like on post ${id} when disliking it')
423 }
424 }
425
426 like := Like{
427 user_id: user.id
428 post_id: post.id
429 is_like: false
430 }
431 sql app.db {
432 insert like into Like
433 // yeet the old cached like value
434 delete from LikeCache where post_id == post.id
435 } or {
436 eprintln('user ${user.id} failed to dislike post ${id}')
437 return ctx.server_error('failed to dislike post')
438 }
439 return ctx.ok('disliked post')
440 }
441}
442
443////// Site //////
444
445@['/api/site/set_motd'; post]
446fn (mut app App) api_site_set_motd(mut ctx Context, motd string) veb.Result {
447 user := app.whoami(mut ctx) or {
448 ctx.error('not logged in!')
449 return ctx.redirect('/login')
450 }
451
452 if user.admin {
453 sql app.db {
454 update Site set motd = motd where id == 1
455 } or {
456 ctx.error('failed to set motd')
457 eprintln('failed to set motd: ${motd}')
458 return ctx.redirect('/')
459 }
460 println('set motd to: ${motd}')
461 return ctx.redirect('/')
462 } else {
463 ctx.error('insufficient permissions!')
464 eprintln('insufficient perms to set motd to: ${motd} (${user.id})')
465 return ctx.redirect('/')
466 }
467}