a mini social media app for small communities

add flag for automated accounts and fix password fields using username configs

Changed files
+127 -64
doc
src
database
entity
templates
webapp
+1 -1
config.maple
··· 48 48 49 49 password_min_len = 12 50 50 password_max_len = 72 51 - password_pattern = '(.|\s)+' 51 + password_pattern = '.+' 52 52 53 53 pronouns_min_len = 0 54 54 pronouns_max_len = 30
+1
doc/database_spec.md
··· 18 18 | `password_salt` | string | salt for this user's password | 19 19 | `muted` | bool | controls whether or not this user can make posts | 20 20 | `admin` | bool | controls whether or not this user is an admin | 21 + | `automated` | bool | controls whether or not this user is automated | 21 22 | `theme` | ?string | controls per-user css themes | 22 23 | `bio` | string | bio for this user | 23 24 | `pronouns` | string | pronouns for this user |
+12
src/database/user.v
··· 65 65 return true 66 66 } 67 67 68 + // set_automated sets the given user's automated status, returns true if this 69 + // succeeded and false otherwise. 70 + pub fn (app &DatabaseAccess) set_automated(user_id int, automated bool) bool { 71 + sql app.db { 72 + update User set automated = automated where id == user_id 73 + } or { 74 + eprintln('failed to update automated status for ${user_id}') 75 + return false 76 + } 77 + return true 78 + } 79 + 68 80 // set_theme sets the given user's theme url, returns true if this succeeded and 69 81 // false otherwise. 70 82 pub fn (app &DatabaseAccess) set_theme(user_id int, theme ?string) bool {
+3 -2
src/entity/user.v
··· 13 13 password string 14 14 password_salt string 15 15 16 - muted bool 17 - admin bool 16 + muted bool 17 + admin bool 18 + automated bool 18 19 19 20 theme string 20 21
+7 -7
src/main.v
··· 43 43 44 44 @[inline] 45 45 fn load_validators(mut app App) { 46 - app.validators.username = StringValidator.new(app.config.user.username_min_len, app.config.user.username_max_len, app.config.user.username_pattern) 47 - app.validators.password = StringValidator.new(app.config.user.username_min_len, app.config.user.username_max_len, app.config.user.username_pattern) 48 - app.validators.nickname = StringValidator.new(app.config.user.nickname_min_len, app.config.user.nickname_max_len, app.config.user.nickname_pattern) 49 - app.validators.user_bio = StringValidator.new(app.config.user.bio_min_len, app.config.user.bio_max_len, app.config.user.bio_pattern) 50 - app.validators.pronouns = StringValidator.new(app.config.user.pronouns_min_len, app.config.user.pronouns_max_len, app.config.user.pronouns_pattern) 51 - app.validators.post_title = StringValidator.new(app.config.post.title_min_len, app.config.post.title_max_len, app.config.post.title_pattern) 52 - app.validators.post_body = StringValidator.new(app.config.post.body_min_len, app.config.post.body_max_len, app.config.post.body_pattern) 46 + app.validators.username = StringValidator.new(app.config.user.username_min_len,app.config.user.username_max_len,app.config.user.username_pattern) 47 + app.validators.password = StringValidator.new(app.config.user.password_min_len,app.config.user.password_max_len,app.config.user.password_pattern) 48 + app.validators.nickname = StringValidator.new(app.config.user.nickname_min_len,app.config.user.nickname_max_len,app.config.user.nickname_pattern) 49 + app.validators.user_bio = StringValidator.new(app.config.user.bio_min_len,app.config.user.bio_max_len,app.config.user.bio_pattern) 50 + app.validators.pronouns = StringValidator.new(app.config.user.pronouns_min_len,app.config.user.pronouns_max_len,app.config.user.pronouns_pattern) 51 + app.validators.post_title = StringValidator.new(app.config.post.title_min_len,app.config.post.title_max_len,app.config.post.title_pattern) 52 + app.validators.post_body = StringValidator.new(app.config.post.body_min_len,app.config.post.body_max_len,app.config.post.body_pattern) 53 53 } 54 54 55 55 fn main() {
+23
src/templates/settings.html
··· 94 94 95 95 <hr> 96 96 97 + <form action="/api/user/set_automated" method="post"> 98 + <label for="is_automated">is automated:</label> 99 + <input 100 + type="checkbox" 101 + name="is_automated" 102 + id="is_automated" 103 + value="true" 104 + @if user.automated 105 + checked aria-checked 106 + @end 107 + > 108 + <input type="submit" value="save"> 109 + <p>automated accounts are primarily intended to tell users that this account makes posts automatically.</p> 110 + <p>it will also hide most front-end interactions since the user of this account likely will not be using those very often.</p> 111 + </form> 112 + 113 + <hr> 114 + 97 115 <details> 98 116 <summary>dangerous settings (click to reveal)</summary> 99 117 118 + <br> 119 + 100 120 <details> 101 121 <summary>change password (click to reveal)</summary> 102 122 <form action="/api/user/set_password" method="post"> ··· 112 132 required aria-required 113 133 autocomplete="off" aria-autocomplete="off" 114 134 > 135 + <br> 115 136 <label for="new_password">new password:</label> 116 137 <input 117 138 type="password" ··· 126 147 <input type="submit" value="save"> 127 148 </form> 128 149 </details> 150 + 151 + <br> 129 152 130 153 <details> 131 154 <summary>account deletion (click to reveal)</summary>
+11 -6
src/templates/user.html
··· 8 8 (@viewing.pronouns) 9 9 @end 10 10 11 - @if viewing.muted && viewing.admin 12 - [muted admin, somehow] 13 - @else if viewing.muted 11 + @if viewing.muted 14 12 [muted] 15 - @else if viewing.admin 13 + @end 14 + 15 + @if viewing.automated 16 + [automated] 17 + @end 18 + 19 + @if viewing.admin 16 20 [admin] 17 21 @end 18 22 </h1> 19 23 20 24 @if app.logged_in_as(mut ctx, viewing.id) 21 - <script src="/static/js/text_area_counter.js"></script> 22 - 23 25 <p>this is you!</p> 24 26 27 + @if !user.automated 28 + <script src="/static/js/text_area_counter.js"></script> 25 29 <div> 26 30 <form action="/api/post/new_post" method="post"> 27 31 <h2>new post:</h2> ··· 63 67 </script> 64 68 </div> 65 69 <hr> 70 + @end 66 71 @end 67 72 68 73 @if viewing.bio != ''
+69 -48
src/webapp/api.v
··· 49 49 ) 50 50 token := app.auth.add_token(x.id, ctx.ip()) or { 51 51 eprintln(err) 52 - ctx.error('could not create token for user with id ${x.id}') 52 + ctx.error('api_user_register: could not create token for user with id ${x.id}') 53 53 return ctx.redirect('/') 54 54 } 55 55 ctx.set_cookie( ··· 60 60 path: '/' 61 61 ) 62 62 } else { 63 - eprintln('could not log into newly-created user: ${user}') 63 + eprintln('api_user_register: could not log into newly-created user: ${user}') 64 64 ctx.error('could not log into newly-created user.') 65 65 } 66 66 ··· 110 110 return ctx.redirect('/settings') 111 111 } 112 112 113 - // invalidate tokens 113 + hashed_new_password := auth.hash_password_with_salt(new_password, user.password_salt) 114 + if !app.set_password(user.id, hashed_new_password) { 115 + ctx.error('failed to update password') 116 + return ctx.redirect('/settings') 117 + } 118 + 119 + // invalidate tokens and log out 114 120 app.auth.delete_tokens_for_user(user.id) or { 115 121 eprintln('failed to yeet tokens during password deletion for ${user.id} (${err})') 116 122 return ctx.redirect('/settings') 117 123 } 118 - 119 - hashed_new_password := auth.hash_password_with_salt(new_password, user.password_salt) 120 - 121 - if !app.set_password(user.id, hashed_new_password) { 122 - ctx.error('failed to update password') 123 - } 124 + ctx.set_cookie( 125 + name: 'token' 126 + value: '' 127 + same_site: .same_site_none_mode 128 + secure: true 129 + path: '/' 130 + ) 124 131 125 132 return ctx.redirect('/login') 126 133 } ··· 257 264 } 258 265 } 259 266 267 + @['/api/user/set_automated'; post] 268 + fn (mut app App) api_user_set_automated(mut ctx Context, is_automated bool) veb.Result { 269 + user := app.whoami(mut ctx) or { 270 + ctx.error('you are not logged in!') 271 + return ctx.redirect('/login') 272 + } 273 + 274 + if !app.set_automated(user.id, is_automated) { 275 + ctx.error('failed to set automated status.') 276 + } 277 + 278 + return ctx.redirect('/me') 279 + } 280 + 260 281 @['/api/user/set_theme'; post] 261 282 fn (mut app App) api_user_set_theme(mut ctx Context, url string) veb.Result { 262 283 if !app.config.instance.allow_changing_theme { ··· 330 351 return ctx.text(user.get_name()) 331 352 } 332 353 333 - /// user/notification /// 334 - 335 - @['/api/user/notification/clear'] 336 - fn (mut app App) api_user_notification_clear(mut ctx Context, id int) veb.Result { 337 - user := app.whoami(mut ctx) or { 338 - ctx.error('you are not logged in!') 339 - return ctx.redirect('/login') 340 - } 341 - 342 - if notification := app.get_notification_by_id(id) { 343 - if notification.user_id != user.id { 344 - ctx.error('no such notification for user') 345 - return ctx.redirect('/inbox') 346 - } else { 347 - if !app.delete_notification(id) { 348 - ctx.error('failed to delete notification') 349 - return ctx.redirect('/inbox') 350 - } 351 - } 352 - } else { 353 - ctx.error('no such notification for user') 354 - } 355 - 356 - return ctx.redirect('/inbox') 357 - } 358 - 359 - @['/api/user/notification/clear_all'] 360 - fn (mut app App) api_user_notification_clear_all(mut ctx Context) veb.Result { 361 - user := app.whoami(mut ctx) or { 362 - ctx.error('you are not logged in!') 363 - return ctx.redirect('/login') 364 - } 365 - if !app.delete_notifications_for_user(user.id) { 366 - ctx.error('failed to delete notifications') 367 - return ctx.redirect('/inbox') 368 - } 369 - return ctx.redirect('/inbox') 370 - } 371 - 372 354 @['/api/user/delete'] 373 355 fn (mut app App) api_user_delete(mut ctx Context, id int) veb.Result { 374 356 user := app.whoami(mut ctx) or { ··· 413 395 } 414 396 users := app.search_for_users(query, limit, offset) 415 397 return ctx.json[[]User](users) 398 + } 399 + 400 + /// user/notification /// 401 + 402 + @['/api/user/notification/clear'] 403 + fn (mut app App) api_user_notification_clear(mut ctx Context, id int) veb.Result { 404 + user := app.whoami(mut ctx) or { 405 + ctx.error('you are not logged in!') 406 + return ctx.redirect('/login') 407 + } 408 + 409 + if notification := app.get_notification_by_id(id) { 410 + if notification.user_id != user.id { 411 + ctx.error('no such notification for user') 412 + return ctx.redirect('/inbox') 413 + } else { 414 + if !app.delete_notification(id) { 415 + ctx.error('failed to delete notification') 416 + return ctx.redirect('/inbox') 417 + } 418 + } 419 + } else { 420 + ctx.error('no such notification for user') 421 + } 422 + 423 + return ctx.redirect('/inbox') 424 + } 425 + 426 + @['/api/user/notification/clear_all'] 427 + fn (mut app App) api_user_notification_clear_all(mut ctx Context) veb.Result { 428 + user := app.whoami(mut ctx) or { 429 + ctx.error('you are not logged in!') 430 + return ctx.redirect('/login') 431 + } 432 + if !app.delete_notifications_for_user(user.id) { 433 + ctx.error('failed to delete notifications') 434 + return ctx.redirect('/inbox') 435 + } 436 + return ctx.redirect('/inbox') 416 437 } 417 438 418 439 ////// post //////