a mini social media app for small communities

add ability to change username/password and give log out its own page

Changed files
+181 -56
doc
src
+2 -2
doc/todo.md
··· 11 11 12 12 - [ ] post:images (should have a config.maple toggle to enable/disable) 13 13 - [ ] post:saving (add the post to a list of saved posts that a user can view later) 14 - - [ ] user:change password 15 - - [ ] user:change username 16 14 17 15 ## ideas 18 16 ··· 26 24 - [x] user:listed pronouns 27 25 - [x] user:notifications 28 26 - [x] user:deletion 27 + - [x] user:change password 28 + - [x] user:change username 29 29 - [x] post:likes/dislikes 30 30 - [x] post:mentioning ('tagging') other users in posts 31 31 - [x] post:mentioning:who mentioned you (send notifications when a user mentions you)
+65 -1
src/api.v
··· 71 71 return ctx.redirect('/') 72 72 } 73 73 74 + @['/api/user/set_username'; post] 75 + fn (mut app App) api_user_set_username(mut ctx Context, new_username string) veb.Result { 76 + user := app.whoami(mut ctx) or { 77 + ctx.error('you are not logged in!') 78 + return ctx.redirect('/login') 79 + } 80 + 81 + if app.get_user_by_name(new_username) != none { 82 + ctx.error('username taken') 83 + return ctx.redirect('/settings') 84 + } 85 + 86 + // validate username 87 + if !app.validators.username.validate(new_username) { 88 + ctx.error('invalid username') 89 + return ctx.redirect('/settings') 90 + } 91 + 92 + sql app.db { 93 + update User set username = new_username where id == user.id 94 + } or { 95 + eprintln('failed to update username for ${user.id}') 96 + ctx.error('failed to update username') 97 + } 98 + 99 + return ctx.redirect('/settings') 100 + } 101 + 102 + @['/api/user/set_password'; post] 103 + fn (mut app App) api_user_set_password(mut ctx Context, current_password string, new_password string) veb.Result { 104 + user := app.whoami(mut ctx) or { 105 + ctx.error('you are not logged in!') 106 + return ctx.redirect('/login') 107 + } 108 + 109 + if !auth.compare_password_with_hash(current_password, user.password_salt, user.password) { 110 + ctx.error('current_password is incorrect') 111 + return ctx.redirect('/settings') 112 + } 113 + 114 + // validate password 115 + if !app.validators.password.validate(new_password) { 116 + ctx.error('invalid password') 117 + return ctx.redirect('/settings') 118 + } 119 + 120 + // invalidate tokens 121 + app.auth.delete_tokens_for_user(user.id) or { 122 + eprintln('failed to yeet tokens during password deletion for ${user.id} (${err})') 123 + return ctx.redirect('/settings') 124 + } 125 + 126 + hashed_new_password := auth.hash_password_with_salt(new_password, user.password_salt) 127 + 128 + sql app.db { 129 + update User set password = hashed_new_password where id == user.id 130 + } or { 131 + eprintln('failed to update password for ${user.id}') 132 + ctx.error('failed to update password') 133 + } 134 + 135 + return ctx.redirect('/login') 136 + } 137 + 74 138 @['/api/user/login'; post] 75 139 fn (mut app App) api_user_login(mut ctx Context, username string, password string) veb.Result { 76 140 user := app.get_user_by_name(username) or { ··· 105 169 if token := ctx.get_cookie('token') { 106 170 if user := app.get_user_by_token(ctx, token) { 107 171 app.auth.delete_tokens_for_ip(ctx.ip()) or { 108 - eprintln('failed to yeet tokens for ${user.id} with ip ${ctx.ip()}') 172 + eprintln('failed to yeet tokens for ${user.id} with ip ${ctx.ip()} (${err})') 109 173 return ctx.redirect('/login') 110 174 } 111 175 } else {
+9
src/pages.v
··· 58 58 return $veb.html() 59 59 } 60 60 61 + fn (mut app App) logout(mut ctx Context) veb.Result { 62 + user := app.whoami(mut ctx) or { 63 + ctx.error('not logged in') 64 + return ctx.redirect('/login') 65 + } 66 + ctx.title = '${app.config.instance.name} logout' 67 + return $veb.html() 68 + } 69 + 61 70 @['/user/:username'] 62 71 fn (mut app App) user(mut ctx Context, username string) veb.Result { 63 72 user := app.whoami(mut ctx) or { User{} }
+17
src/templates/logout.html
··· 1 + @include 'partial/header.html' 2 + 3 + @if ctx.is_logged_in() 4 + <h1>log out</h1> 5 + 6 + <p>you are currently logged in as: @{user.get_name()}</p> 7 + 8 + <div> 9 + <p><a href="/api/user/logout">log out</a></p> 10 + <hr> 11 + <p><a href="/api/user/full_logout">log out of all devices</a></p> 12 + </div> 13 + @else 14 + <p>you need to be logged in to log out!</p> 15 + @end 16 + 17 + @include 'partial/footer.html'
+1 -1
src/templates/partial/footer.html
··· 9 9 <a href="/admin">admin</a> 10 10 @end 11 11 - 12 - <a href="/api/user/logout">log out</a> 12 + <a href="/logout">log out</a> 13 13 </p> 14 14 @end 15 15
+87 -41
src/templates/settings.html
··· 13 13 rows="10" 14 14 minlength="@app.config.user.bio_min_len" 15 15 maxlength="@app.config.user.bio_max_len" 16 - required 16 + required aria-required 17 17 >@user.bio</textarea> 18 18 <br> 19 19 <input type="submit" value="save"> ··· 31 31 maxlength="@app.config.user.pronouns_max_len" 32 32 pattern="@app.config.user.pronouns_pattern" 33 33 value="@user.pronouns" 34 - required 34 + required aria-required 35 35 > 36 36 <input type="submit" value="save"> 37 37 </form> ··· 48 48 minlength="@app.config.user.nickname_min_len" 49 49 maxlength="@app.config.user.nickname_max_len" 50 50 value="@{user.nickname or { '' }}" 51 - required 51 + required aria-required 52 52 > 53 53 <input type="submit" value="save"> 54 54 </form> ··· 59 59 60 60 @if app.config.instance.allow_changing_theme 61 61 <hr> 62 + 62 63 <form action="/api/user/set_theme" method="post"> 63 64 <label for="url">theme:</label> 64 65 <input type="url" name="url" id="url" value="@{user.theme or { '' }}"> ··· 68 69 69 70 <hr> 70 71 72 + <form action="/api/user/set_username" method="post"> 73 + <label for="new_username">username:</label> 74 + <input 75 + type="text" 76 + name="new_username" 77 + id="new_username" 78 + pattern="@app.config.user.username_pattern" 79 + minlength="@app.config.user.username_min_len" 80 + maxlength="@app.config.user.username_max_len" 81 + value="@{user.username}" 82 + required aria-required 83 + > 84 + <input type="submit" value="save"> 85 + </form> 86 + 87 + <hr> 88 + 71 89 <details> 72 90 <summary>dangerous settings (click to reveal)</summary> 73 - <div> 74 - <details> 75 - <summary>account deletion (click to reveal)</summary> 76 - <form action="/api/user/delete" autocomplete="off"> 77 - <input 78 - type="number" 79 - name="id" 80 - id="id" 81 - value="@user.id" 82 - required 83 - readonly 84 - hidden 85 - aria-hidden 86 - > 87 - <p><strong>there is NO GOING BACK after deleting your account.</strong></p> 88 - <p><strong>EVERY ONE of your posts, notifications, likes, dislikes, and ALL OTHER USER DATA WILL BE PERMANENTLY DELETED</strong></p> 89 - <div> 90 - <input type="checkbox" name="are-you-sure" id="are-you-sure" required> 91 - <label for="are-you-sure">click to confirm</label> 92 - </div> 93 - <br> 94 - <div> 95 - <input type="checkbox" name="are-you-really-sure" id="are-you-really-sure" required> 96 - <label for="are-you-really-sure">click to doubly confirm</label> 97 - </div> 98 - <br> 99 - <div> 100 - <input type="checkbox" name="are-you-absolutely-sure" id="are-you-absolutely-sure" required> 101 - <label for="are-you-absolutely-sure">click to triply confirm</label> 102 - </div> 103 - <br> 104 - <details> 105 - <summary>(click to reveal deletion button)</summary> 106 - <input type="submit" value="delete your account"> 107 - </details> 108 - </form> 109 - </details> 110 - </div> 91 + 92 + <details> 93 + <summary>change password (click to reveal)</summary> 94 + <form action="/api/user/set_password" method="post"> 95 + <p>changing your password will log you out of all devices, so you will need to log in again after changing it.</p> 96 + <label for="current_password">current password:</label> 97 + <input 98 + type="password" 99 + name="current_password" 100 + id="current_password" 101 + pattern="@app.config.user.password_pattern" 102 + minlength="@app.config.user.password_min_len" 103 + maxlength="@app.config.user.password_max_len" 104 + required aria-required 105 + autocomplete="off" aria-autocomplete="off" 106 + > 107 + <label for="new_password">new password:</label> 108 + <input 109 + type="password" 110 + name="new_password" 111 + id="new_password" 112 + pattern="@app.config.user.password_pattern" 113 + minlength="@app.config.user.password_min_len" 114 + maxlength="@app.config.user.password_max_len" 115 + required aria-required 116 + autocomplete="off" aria-autocomplete="off" 117 + > 118 + <input type="submit" value="save"> 119 + </form> 120 + </details> 121 + 122 + <details> 123 + <summary>account deletion (click to reveal)</summary> 124 + <form action="/api/user/delete" autocomplete="off"> 125 + <input 126 + type="number" 127 + name="id" 128 + id="id" 129 + value="@user.id" 130 + required aria-required 131 + readonly aria-readonly 132 + hidden aria-hidden 133 + > 134 + <p><strong>there is NO GOING BACK after deleting your account.</strong></p> 135 + <p><strong>EVERY ONE of your posts, notifications, likes, dislikes, and ALL OTHER USER DATA WILL BE PERMANENTLY DELETED</strong></p> 136 + <div> 137 + <input type="checkbox" name="are-you-sure" id="are-you-sure" required aria-required> 138 + <label for="are-you-sure">click to confirm</label> 139 + </div> 140 + <br> 141 + <div> 142 + <input type="checkbox" name="are-you-really-sure" id="are-you-really-sure" required aria-required> 143 + <label for="are-you-really-sure">click to doubly confirm</label> 144 + </div> 145 + <br> 146 + <div> 147 + <input type="checkbox" name="are-you-absolutely-sure" id="are-you-absolutely-sure" required aria-required> 148 + <label for="are-you-absolutely-sure">click to triply confirm</label> 149 + </div> 150 + <br> 151 + <details> 152 + <summary>(click to reveal deletion button)</summary> 153 + <input type="submit" value="delete your account"> 154 + </details> 155 + </form> 156 + </details> 111 157 </details> 112 158 113 159 @else
-11
src/templates/user.html
··· 67 67 @end 68 68 </div> 69 69 70 - <div> 71 - <h2>user info:</h2> 72 - <p>id: @viewing.id</p> 73 - <p>username: @viewing.username</p> 74 - <p>display name: @viewing.get_name()</p> 75 - @if app.logged_in_as(mut ctx, viewing.id) 76 - <p><a href="/api/user/logout">log out</a></p> 77 - <p><a href="/api/user/full_logout">log out of all devices</a></p> 78 - @end 79 - </div> 80 - 81 70 @if ctx.is_logged_in() && user.admin 82 71 <div> 83 72 <h2>admin powers:</h2>