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}