a mini social media app for small communities

display error notifications from the server, aggressive modifications to the default theme, add nsfw toggle to post editor,

Changed files
+494 -273
src
+2 -2
src/database/post.v
··· 109 109 110 110 // update_post updates the given post's title and body with the given title and 111 111 // body, returns true if this succeeds and false otherwise. 112 - pub fn (app &DatabaseAccess) update_post(post_id int, new_title string, new_body string) bool { 112 + pub fn (app &DatabaseAccess) update_post(post_id int, new_title string, new_body string, new_nsfw bool) bool { 113 113 sql app.db { 114 - update Post set body = new_body, title = new_title where id == post_id 114 + update Post set body = new_body, title = new_title, nsfw = new_nsfw where id == post_id 115 115 } or { 116 116 return false 117 117 }
+52
src/static/js/form.js
··· 1 + async function _submit(event, element) 2 + { 3 + event.preventDefault(); 4 + 5 + /* debug */ 6 + console.log(`submitting form:`); 7 + console.log(element) 8 + console.log(`destination: ${element.action}`); 9 + const formdata = new FormData(element); 10 + console.log(`data:`); 11 + console.log(formdata); 12 + 13 + try 14 + { 15 + await fetch(element.action, { 16 + method: "POST", 17 + headers: { 18 + "Content-Type": "application/x-www-form-urlencoded", 19 + }, 20 + body: new URLSearchParams(new FormData(element)), 21 + }).then(async response => { 22 + console.log(response); 23 + const ok = response.status == 200; 24 + const text = await response.text(); 25 + notify(text, ok ? 'ok' : 'error'); /* /static/js/notify.js */ 26 + if (ok) 27 + { 28 + if (element.hasAttribute("beep-redirect")) 29 + window.location.href = element.getAttribute("beep-redirect"); 30 + else if (element.hasAttribute('beep-redirect-js')) 31 + window.location.href = eval(element.getAttribute("beep-redirect-js"))( 32 + response, 33 + text 34 + ); 35 + } 36 + }); 37 + } 38 + catch (error) 39 + { 40 + console.error(error.message); 41 + } 42 + } 43 + 44 + const e = document.getElementsByTagName('form'); 45 + for (let i = 0 ; i < e.length ; i++) 46 + { 47 + const element = e.item(i); 48 + if (element.method == 'post') 49 + { 50 + element.onsubmit = event => _submit(event, element); 51 + } 52 + }
+18
src/static/js/notify.js
··· 1 + const errors = document.getElementById('errors') 2 + 3 + const notify = (msg, level = 'ok') => { 4 + const p = document.createElement('p'); 5 + p.classList.add(level); 6 + 7 + const button = document.createElement('button'); 8 + button.innerText = 'X'; 9 + button.style.display = 'inline'; 10 + button.onclick = () => errors.removeChild(p); 11 + 12 + const span = document.createElement('span'); 13 + span.innerText = `${level != 'ok' ? `${level}: ` : ''}${msg}`; 14 + 15 + p.appendChild(button); 16 + p.appendChild(span); 17 + errors.appendChild(p); 18 + }
+31
src/static/js/password.js
··· 1 + const add_password_checkers = (password_id, confirm_id, match_id) => { 2 + const password = document.getElementById(password_id); 3 + const confirm_password = document.getElementById(confirm_id); 4 + const matches = document.getElementById(match_id); 5 + 6 + const a = () => { 7 + matches.innerText = password.value==confirm_password.value ? "yes" : "no"; 8 + }; 9 + password.addEventListener('input', a); 10 + confirm_password.addEventListener('input', a); 11 + 12 + const view_password = document.getElementById(`view-${password_id}`); 13 + const view_confirm_password = document.getElementById(`view-${confirm_id}`); 14 + 15 + const b = (elm, btn) => { 16 + return _ => { 17 + if (elm.getAttribute('type') == 'password') 18 + { 19 + elm.setAttribute('type', 'text'); 20 + btn.value = 'hide'; 21 + } 22 + else 23 + { 24 + elm.setAttribute('type', 'password') 25 + btn.value = 'show'; 26 + } 27 + }; 28 + }; 29 + view_password.addEventListener('click', b(password, view_password)); 30 + view_confirm_password.addEventListener('click', b(confirm_password, view_confirm_password)); 31 + }
+5
src/static/style.css
··· 37 37 margin-left: 6px; 38 38 } 39 39 40 + details>summary:hover { 41 + cursor: pointer; 42 + } 43 + 40 44 /* 41 45 * some themes make input fields display: block, which overrides my hidden 42 46 * attribute. to resolve that, i will just override the override. 43 47 */ 44 48 input[hidden] { 45 49 display: none !important; 50 + visibility: none !important; 46 51 }
+159 -26
src/static/themes/default.css
··· 1 - @import url('https://fonts.googleapis.com/css2?family=Nova+Mono&family=Oxygen+Mono&display=swap'); 1 + @import url('https://fonts.googleapis.com/css2?family=Onest:wght@100..900&family=Oxygen+Mono&display=swap'); 2 2 3 3 :root { 4 - --c-bg: #222; 5 - --c-panel-bg: #2c2c2c; 6 - --c-fg: #ffffff; 7 - --c-nsfw-border: #ff6666; 8 - --c-link: #6666ff; 9 - --c-accent: #faaeff; 4 + /* palette */ 5 + /* greys */ 6 + --p-black: #333333; 7 + --p-grey0: #414141; 8 + --p-grey1: #4a4a4a; 9 + --p-grey2: #4f4f4f; 10 + --p-grey3: #5c5c5c; 11 + --p-grey4: #5f5f5f; 12 + --p-white: #e7e7e7; 13 + /* rainbow */ 14 + --p-red: #faa; /* == light red */ 15 + --p-orange: #fa7; 16 + --p-yellow: #ffa; /* == light-orange */ 17 + --p-teal: #7fa; 18 + --p-green: #af7; 19 + --p-blue: #7af; 20 + --p-purple: #a7f; 21 + --p-pink: #f7a; 22 + /* light rainbow */ 23 + --p-light-red: #faa; 24 + --p-light-blue: #aaf; 25 + --p-light-green: #afa; 26 + --p-light-orange: #ffa; /* == yellow */ 27 + --p-light-purple: #faf; 28 + --p-light-blue: #aff; 29 + 30 + /* colours */ 31 + --c-bg: var(--p-black); 32 + --c-panel-bg: var(--p-grey0); 33 + --c-panel-border: var(--p-grey2); 34 + --c-panel2-bg: var(--p-grey1); 35 + --c-panel2-border: var(--p-grey3); 36 + --c-panel3-bg: var(--p-grey2); 37 + --c-panel3-border: var(--p-grey4); 38 + --c-fg: var(--p-white); 39 + --c-nsfw-border: var(--p-orange); 40 + --c-link: var(--p-blue); 41 + --c-link-hover: var(--p-light-blue); 42 + --c-accent: var(--p-light-green); 43 + --c-notify-ok: var(--p-light-green); 44 + --c-notify-error: var(--p-light-red); 45 + 46 + /* text */ 47 + --t-font: 'Onest', Arial, serif; 48 + --t-post-font: Garamond, 'Times New Roman', var(--t-font); 49 + --t-mono-font: 'Oxygen Mono', monospace; 50 + --t-h-font: 'Oxygen Mono', var(--t-post-font); 51 + --t-font-weight: 400; 52 + --t-font-style: normal; 53 + --t-font-size: 20px; 54 + 55 + /* layout */ 56 + --l-body-padding: 16px; 57 + --l-body-gap: 12px; 58 + --l-body-width: 75vw; 59 + --l-border-width: 2px; 60 + --l-border-style: solid; 61 + --l-border-radius: 0px; 10 62 } 11 63 12 64 html { ··· 15 67 margin: 0; 16 68 17 69 width: 100vw; 70 + overflow-x: hidden; 18 71 19 72 display: flex; 20 73 flex-direction: column; ··· 23 76 background-color: var(--c-bg); 24 77 color: var(--c-fg); 25 78 26 - font-family: "Oxygen Mono", sans-serif; 27 - font-weight: 400; 28 - font-style: normal; 29 - font-size: 20px; 79 + font-family: var(--t-font); 80 + font-weight: var(--t-font-weight); 81 + font-style: var(--t-font-style); 82 + font-size: var(--t-font-size); 30 83 } 31 84 32 85 body { 33 - padding: 16px 0 0 0; 86 + padding: var(--l-body-padding) 0 var(--l-body-padding) 0; 34 87 offset: 0; 35 88 margin: 0; 36 - 37 - width: 80vw; 89 + width: var(--l-body-width); 38 90 } 39 91 40 92 header { 41 - padding-bottom: 16px; 93 + padding-bottom: var(--l-body-padding); 42 94 } 43 95 44 96 footer { 45 - padding-top: 16px; 97 + padding-top: var(--l-body-padding); 46 98 } 47 99 48 100 main { 49 - padding: 16px; 50 - 101 + padding: var(--l-body-padding); 51 102 background-color: var(--c-panel-bg); 103 + border: var(--l-border-width) var(--l-border-style) var(--c-panel-border); 104 + border-radius: var(--l-border-radius); 52 105 53 106 display: flex; 54 107 flex-direction: column; 55 - gap: 12px; 108 + gap: var(--l-body-gap); 56 109 } 57 110 58 111 form { 59 112 display: flex; 60 113 flex-direction: column; 61 - gap: 12px; 114 + gap: var(--l-body-gap); 115 + } 116 + 117 + button:hover { 118 + cursor: pointer; 62 119 } 63 120 64 121 input, ··· 67 124 background-color: var(--c-panel-bg); 68 125 color: var(--c-fg); 69 126 70 - border: 2px solid var(--c-accent); 127 + border: var(--l-border-width) var(--l-border-style) var(--c-accent); 128 + border-radius: var(--l-border-radius); 71 129 padding: 6px; 130 + 131 + font-family: var(--t-font); 132 + } 133 + 134 + input:hover, 135 + textarea:hover, 136 + button:hover { 137 + border-color: var(--c-fg); 138 + } 139 + 140 + input:focus, 141 + textarea:focus, 142 + button:focus { 143 + background-color: var(--c-accent); 144 + color: var(--c-bg); 72 145 } 73 146 74 147 h1, h2, h3, h4, h5, h6, p { ··· 76 149 } 77 150 78 151 h1, header, footer { 79 - font-family: "Nova Mono", sans-serif; 152 + font-family: var(--t-h-font); 80 153 } 81 154 82 155 a { 83 156 color: var(--c-link); 157 + transition: 0.15s linear color; 158 + } 159 + 160 + a:hover { 161 + color: var(--c-link-hover); 84 162 } 85 163 86 164 hr { 87 165 width: 100%; 88 166 } 89 167 168 + pre { 169 + font-family: var(--t-mono-font); 170 + } 171 + 90 172 .post { 91 173 border: none; 92 - border-left: 2px solid var(--c-fg); 174 + border-left: var(--l-border-width) var(--l-border-style) var(--c-fg); 175 + } 176 + 177 + .post>pre { 178 + font-family: var(--t-post-font); 93 179 } 94 180 95 181 .post + .post, ··· 97 183 margin-top: 18px; 98 184 } 99 185 100 - form, 186 + form:not(.form-inline), 101 187 #recent-posts, 102 188 #pinned-posts { 103 - border-top: 2px solid var(--c-accent); 104 - border-bottom: 2px solid var(--c-accent); 105 189 padding: 16px 24px 16px 24px; 190 + background-color: var(--c-panel2-bg); 191 + border: var(--l-border-width) var(--l-border-style) var(--c-panel2-border); 192 + border-radius: var(--l-border-radius); 193 + } 194 + 195 + #errors:empty { 196 + display: none; 197 + visibility: hidden; 198 + } 199 + 200 + #errors { 201 + display: flex; 202 + flex-direction: column; 203 + gap: var(--l-body-gap); 204 + } 205 + 206 + #errors>p { 207 + background-color: var(--c-panel3-bg); 208 + border: var(--l-border-width) var(--l-border-style) var(--c-panel3-border); 209 + border-radius: var(--l-border-radius); 210 + 211 + padding: 8px; 212 + width: calc(100% - 16px); 213 + 214 + display: inline-flex; 215 + align-items: center; 216 + justify-content: center; 217 + gap: 12px; 218 + } 219 + 220 + #errors>p>button { 221 + border-color: inherit; 222 + flex-grow: 0; 223 + } 224 + 225 + #errors>p>button:hover { 226 + border-color: var(--c-fg); 227 + } 228 + 229 + #errors>p>span { 230 + flex-grow: 1; 231 + } 232 + 233 + #errors>p.ok { 234 + border-color: var(--c-notify-ok); 235 + } 236 + 237 + #errors>p.error { 238 + border-color: var(--c-notify-error); 106 239 }
+7 -1
src/templates/components/new_post.html
··· 1 1 <script src="/static/js/text_area_counter.js"></script> 2 2 <div> 3 - <form action="/api/post/new_post" method="post"> 3 + <form action="/api/post/new_post" method="post" 4 + beep-redirect-js="(_,t)=>{return'/post/'+t.split('=')[1];}"> 5 + <!-- 6 + the above JS snippet will redirect the user to the new post. It's a liiiitle convoluted but whatever. 7 + A successful new_post response will always respond with `posted. id=<id>`. I could just return JSON but honestly I don't care lmao. 8 + TODO: return json because it's definitely better practice. also it would be useful for custom clients :p 9 + --> 4 10 <h2>new post:</h2> 5 11 6 12 @if replying
+19 -2
src/templates/edit.html
··· 7 7 <h1>edit post</h1> 8 8 9 9 <div class="post post-full"> 10 - <form action="/api/post/edit" method="post"> 10 + <form action="/api/post/edit" method="post" beep-redirect="/post/@post.id"> 11 11 <input 12 12 type="number" 13 13 name="id" ··· 47 47 >@post.body</textarea> 48 48 <br> 49 49 50 + @if app.config.post.allow_nsfw 51 + <div> 52 + <label for="nsfw">is nsfw:</label> 53 + <input 54 + type="checkbox" 55 + name="nsfw" 56 + id="nsfw" 57 + @if post.nsfw 58 + checked aria-checked 59 + @end 60 + /> 61 + </div> 62 + <br> 63 + @else 64 + <input type="checkbox" name="nsfw" id="nsfw" hidden aria-hidden /> 65 + @end 66 + 50 67 <input type="submit" value="save"> 51 68 </form> 52 69 </div> ··· 55 72 56 73 <div> 57 74 <h2>danger zone:</h2> 58 - <form action="/api/post/delete" method="post"> 75 + <form action="/api/post/delete" method="post" beep-redirect="/"> 59 76 <input 60 77 type="number" 61 78 name="id"
+10 -3
src/templates/inbox.html
··· 9 9 @if notifications.len == 0 10 10 <p>your inbox is empty!</p> 11 11 @else 12 - <a href="/api/user/notification/clear_all">clear all</a> 12 + <form action="/api/user/notification/clear_all" method="post" beep-redirect="/inbox"> 13 + <button>clear all</button> 14 + </form> 13 15 <hr> 14 16 @for notification in notifications.reverse() 15 17 <div class="notification"> 16 - <p><strong>@notification.summary</strong></p> 18 + <div style="display: flex; flex-direction: row; align-items: center; gap: 12px;"> 19 + <p><strong>@notification.summary</strong></p> 20 + <form action="/api/user/notification/clear" method="post" beep-redirect="/inbox" class="form-inline" style="display: inline;"> 21 + <input type="number" value="@{notification.id}" name="id" required aria-required hidden aria-hidden readonly aria-readonly /> 22 + <button style="display: inline;">clear</button> 23 + </form> 24 + </div> 17 25 <pre id="notif-@{notification.id}">@notification.body</pre> 18 - <a href="/api/user/notification/clear?id=@{notification.id}">clear</a> 19 26 <script> 20 27 render_body('notif-@{notification.id}') 21 28 </script>
+2 -2
src/templates/login.html
··· 11 11 <p>you are already logged in as @{user.get_name()}!</p> 12 12 <a href="/api/user/logout">log out</a> 13 13 @else 14 - <form action="/api/user/login" method="post"> 14 + <form action="/api/user/login" method="post" beep-redirect="/me"> 15 15 <label for="username">username:</label> 16 16 <input 17 17 type="text" ··· 39 39 @end 40 40 </div> 41 41 42 - @include 'partial/footer.html' 42 + @include 'partial/footer.html'
+4 -6
src/templates/partial/header.html
··· 23 23 @else 24 24 <style>@{app.config.instance.default_css}</style> 25 25 @end 26 + 27 + <script src="/static/js/notify.js" defer></script> 28 + <script src="/static/js/form.js" defer></script> 26 29 </head> 27 30 28 31 <body> ··· 53 56 </header> 54 57 55 58 <main> 56 - <!-- TODO: fix this lol --> 57 - @if ctx.form_error != '' 58 - <div> 59 - <p><strong>error:</strong> @ctx.form_error</p> 60 - </div> 61 - @end 59 + <div id="errors"></div>
+1 -3
src/templates/post.html
··· 3 3 <script src="/static/js/post.js"></script> 4 4 <script src="/static/js/render_body.js"></script> 5 5 6 - <br> 7 - 8 6 <div class="post post-full"> 9 7 <h2> 10 8 <a href="/user/@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).username}"><strong>@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).get_name()}</strong></a> ··· 99 97 <input type="submit" value="pin"> 100 98 </form> 101 99 102 - <form action="/api/post/delete" method="post"> 100 + <form action="/api/post/delete" method="post" beep-redirect="/"> 103 101 <input 104 102 type="number" 105 103 name="id"
+4 -30
src/templates/register.html
··· 1 1 @include 'partial/header.html' 2 2 3 + <script src="/static/js/password.js"></script> 4 + 3 5 <h1>register</h1> 4 6 5 7 <div> ··· 11 13 <p>you are already logged in as @{user.get_name()}!</p> 12 14 <a href="/api/user/logout">log out</a> 13 15 @else 14 - <form action="/api/user/register" method="post"> 16 + <form action="/api/user/register" method="post" beep-redirect="/me"> 15 17 <label for="username">username:</label> 16 18 <input 17 19 type="text" ··· 63 65 </div> 64 66 65 67 <script> 66 - const password = document.getElementById('password'); 67 - const confirm_password = document.getElementById('confirm-password'); 68 - const matches = document.getElementById('passwords-match'); 69 - 70 - const a = () => { 71 - matches.innerText = password.value==confirm_password.value ? "yes" : "no"; 72 - }; 73 - password.addEventListener('input', a); 74 - confirm_password.addEventListener('input', a); 75 - 76 - const view_password = document.getElementById('view-password'); 77 - const view_confirm_password = document.getElementById('view-confirm-password'); 78 - 79 - const b = (elm, btn) => { 80 - return event => { 81 - if (elm.getAttribute('type') == 'password') 82 - { 83 - elm.setAttribute('type', 'text'); 84 - btn.innerText = 'hide'; 85 - } 86 - else 87 - { 88 - elm.setAttribute('type', 'password') 89 - btn.innerText = 'show'; 90 - } 91 - }; 92 - }; 93 - view_password.addEventListener('click', b(password, view_password)); 94 - view_confirm_password.addEventListener('click', b(confirm_password, view_confirm_password)); 68 + add_password_checkers('password', 'confirm-password', 'passwords-match'); 95 69 </script> 96 70 97 71 @include 'partial/footer.html'
+22 -3
src/templates/settings.html
··· 2 2 3 3 @if ctx.is_logged_in() 4 4 <script src="/static/js/text_area_counter.js"></script> 5 + <script src="/static/js/password.js"></script> 5 6 6 7 <h1>user settings:</h1> 7 8 ··· 127 128 128 129 <details> 129 130 <summary>change password (click to reveal)</summary> 130 - <form action="/api/user/set_password" method="post"> 131 + <form action="/api/user/set_password" method="post" beep-redirect="/login"> 131 132 <p>changing your password will log you out of all devices, so you will need to log in again after changing it.</p> 132 133 <label for="current_password">current password:</label> 133 134 <input ··· 141 142 autocomplete="off" aria-autocomplete="off" 142 143 > 143 144 <br> 144 - <label for="new_password">new password:</label> 145 + <label for="new_password">new password: <input type="button" id="view-new_password" style="display: inline;" value="view"></input></label> 145 146 <input 146 147 type="password" 147 148 name="new_password" ··· 152 153 required aria-required 153 154 autocomplete="off" aria-autocomplete="off" 154 155 > 156 + <label for="confirm_password">confirm password: <input type="button" id="view-confirm_password" style="display: inline;" value="view"></input></label> 157 + <input 158 + type="password" 159 + name="confirm_password" 160 + id="confirm_password" 161 + pattern="@app.config.user.password_pattern" 162 + minlength="@app.config.user.password_min_len" 163 + maxlength="@app.config.user.password_max_len" 164 + required aria-required 165 + autocomplete="off" aria-autocomplete="off" 166 + > 167 + <br> 168 + <p>passwords match: <span id="passwords-match">yes</span></p> 169 + <br> 155 170 <input type="submit" value="save"> 156 171 </form> 157 172 </details> ··· 160 175 161 176 <details> 162 177 <summary>account deletion (click to reveal)</summary> 163 - <form action="/api/user/delete" autocomplete="off"> 178 + <form action="/api/user/delete" autocomplete="off" beep-redirect="/"> 164 179 <input 165 180 type="number" 166 181 name="id" ··· 194 209 </form> 195 210 </details> 196 211 </details> 212 + 213 + <script> 214 + add_password_checkers('new_password', 'confirm_password', 'passwords-match'); 215 + </script> 197 216 198 217 @else 199 218 <p>uh oh, you need to be logged in to view this page!</p>
+158 -195
src/webapp/api.v
··· 10 10 // search_hard_limit is the maximum limit for a search query, used to prevent 11 11 // people from requesting searches with huge limits and straining the SQL server 12 12 pub const search_hard_limit = 50 13 + pub const not_logged_in_msg = 'you are not logged in!' 13 14 14 15 ////// user ////// 15 16 ··· 29 30 'remoteip': ctx.ip() 30 31 'response': token 31 32 }) or { 32 - ctx.error('failed to post hcaptcha response: ${err}') 33 - return ctx.redirect('/register') 33 + return ctx.server_error('failed to post hcaptcha response: ${err}') 34 34 } 35 35 data := json.decode(HcaptchaResponse, response.body) or { 36 - ctx.error('failed to decode hcaptcha response: ${err}') 37 - return ctx.redirect('/register') 36 + return ctx.server_error('failed to decode hcaptcha response: ${err}') 38 37 } 39 38 if !data.success { 40 - ctx.error('failed to verify hcaptcha: ${data}') 41 - return ctx.redirect('/register') 39 + return ctx.server_error('failed to verify hcaptcha: ${data}') 42 40 } 43 41 } 44 42 45 43 if app.config.instance.invite_only && ctx.form['invite-code'] != app.config.instance.invite_code { 46 - ctx.error('invalid invite code') 47 - return ctx.redirect('/register') 44 + return ctx.server_error('invalid invite code') 48 45 } 49 46 50 47 if app.get_user_by_name(username) != none { 51 - ctx.error('username taken') 52 - return ctx.redirect('/register') 48 + return ctx.server_error('username taken') 53 49 } 54 50 55 51 // validate username 56 52 if !app.validators.username.validate(username) { 57 - ctx.error('invalid username') 58 - return ctx.redirect('/register') 53 + return ctx.server_error('invalid username') 59 54 } 60 55 61 56 // validate password 62 57 if !app.validators.password.validate(password) { 63 - ctx.error('invalid password') 64 - return ctx.redirect('/register') 58 + return ctx.server_error('invalid password') 65 59 } 66 60 67 61 if password != ctx.form['confirm-password'] { 68 - ctx.error('passwords do not match') 69 - return ctx.redirect('/register') 62 + return ctx.server_error('passwords do not match') 70 63 } 71 64 72 65 salt := auth.generate_salt() ··· 84 77 app.send_notification_to(x.id, app.config.welcome.summary.replace('%s', x.get_name()), 85 78 app.config.welcome.body.replace('%s', x.get_name())) 86 79 token := app.auth.add_token(x.id) or { 87 - eprintln(err) 88 - ctx.error('api_user_register: could not create token for user with id ${x.id}') 89 - return ctx.redirect('/') 80 + eprintln('api_user_register: could not create token for user with id ${x.id}: ${err}') 81 + return ctx.server_error('could not create token for user') 90 82 } 91 83 ctx.set_cookie( 92 84 name: 'token' ··· 97 89 ) 98 90 } else { 99 91 eprintln('api_user_register: could not log into newly-created user: ${user}') 100 - ctx.error('could not log into newly-created user.') 92 + return ctx.server_error('could not log into newly-created user.') 101 93 } 102 94 103 - return ctx.redirect('/') 95 + return ctx.ok('user registered') 104 96 } 105 97 106 98 @['/api/user/set_username'; post] 107 99 fn (mut app App) api_user_set_username(mut ctx Context, new_username string) veb.Result { 108 100 user := app.whoami(mut ctx) or { 109 - ctx.error('you are not logged in!') 110 - return ctx.redirect('/login') 101 + return ctx.unauthorized(not_logged_in_msg) 111 102 } 112 103 113 104 if app.get_user_by_name(new_username) != none { 114 - ctx.error('username taken') 115 - return ctx.redirect('/settings') 105 + return ctx.server_error('username taken') 116 106 } 117 107 118 108 // validate username 119 109 if !app.validators.username.validate(new_username) { 120 - ctx.error('invalid username') 121 - return ctx.redirect('/settings') 110 + return ctx.server_error('invalid username') 122 111 } 123 112 124 113 if !app.set_username(user.id, new_username) { 125 - ctx.error('failed to update username') 114 + return ctx.server_error('failed to update username') 126 115 } 127 116 128 - return ctx.redirect('/settings') 117 + return ctx.ok('username updated') 129 118 } 130 119 131 120 @['/api/user/set_password'; post] 132 121 fn (mut app App) api_user_set_password(mut ctx Context, current_password string, new_password string) veb.Result { 133 122 user := app.whoami(mut ctx) or { 134 - ctx.error('you are not logged in!') 135 - return ctx.redirect('/login') 123 + return ctx.unauthorized(not_logged_in_msg) 136 124 } 137 125 138 126 if !auth.compare_password_with_hash(current_password, user.password_salt, user.password) { 139 - ctx.error('current_password is incorrect') 140 - return ctx.redirect('/settings') 127 + return ctx.server_error('current_password is incorrect') 141 128 } 142 129 143 130 // validate password 144 131 if !app.validators.password.validate(new_password) { 145 - ctx.error('invalid password') 146 - return ctx.redirect('/settings') 132 + return ctx.server_error('invalid password') 133 + } 134 + 135 + if new_password != ctx.form['confirm_password'] { 136 + return ctx.server_error('passwords do not match') 147 137 } 148 138 149 139 hashed_new_password := auth.hash_password_with_salt(new_password, user.password_salt) 150 140 if !app.set_password(user.id, hashed_new_password) { 151 - ctx.error('failed to update password') 152 - return ctx.redirect('/settings') 141 + return ctx.server_error('failed to update password') 153 142 } 154 143 155 144 // invalidate tokens and log out 156 145 app.auth.delete_tokens_for_user(user.id) or { 157 146 eprintln('failed to yeet tokens during password deletion for ${user.id} (${err})') 158 - return ctx.redirect('/settings') 147 + return ctx.server_error('failed to delete tokens during password deletion') 159 148 } 160 149 ctx.set_cookie( 161 150 name: 'token' ··· 165 154 path: '/' 166 155 ) 167 156 168 - return ctx.redirect('/login') 157 + return ctx.ok('password updated') 169 158 } 170 159 171 160 @['/api/user/login'; post] 172 161 fn (mut app App) api_user_login(mut ctx Context, username string, password string) veb.Result { 173 162 user := app.get_user_by_name(username) or { 174 - ctx.error('invalid credentials') 175 - return ctx.redirect('/login') 163 + return ctx.server_error('invalid credentials') 176 164 } 177 165 178 166 if !auth.compare_password_with_hash(password, user.password_salt, user.password) { 179 - ctx.error('invalid credentials') 180 - return ctx.redirect('/login') 167 + return ctx.server_error('invalid credentials') 181 168 } 182 169 183 170 token := app.auth.add_token(user.id) or { 184 171 eprintln('failed to add token on log in: ${err}') 185 - ctx.error('could not create token for user with id ${user.id}') 186 - return ctx.redirect('/login') 172 + return ctx.server_error('could not create token for user with id ${user.id}') 187 173 } 188 174 189 175 ctx.set_cookie( ··· 194 180 path: '/' 195 181 ) 196 182 197 - return ctx.redirect('/') 183 + return ctx.ok('logged in') 198 184 } 199 185 200 - @['/api/user/logout'] 186 + @['/api/user/logout'; post] 201 187 fn (mut app App) api_user_logout(mut ctx Context) veb.Result { 202 188 if token := ctx.get_cookie('token') { 203 189 if user := app.get_user_by_token(token) { ··· 207 193 // } 208 194 app.auth.delete_tokens_for_value(token) or { 209 195 eprintln('failed to yeet tokens for ${user.id} with ip ${ctx.ip()} (${err})') 210 - return ctx.redirect('/login') 211 196 } 212 197 } else { 213 198 eprintln('failed to get user for token for logout') ··· 224 209 path: '/' 225 210 ) 226 211 227 - return ctx.redirect('/login') 212 + return ctx.ok('logged out') 228 213 } 229 214 230 - @['/api/user/full_logout'] 215 + @['/api/user/full_logout'; post] 231 216 fn (mut app App) api_user_full_logout(mut ctx Context) veb.Result { 232 217 if token := ctx.get_cookie('token') { 233 218 if user := app.get_user_by_token(token) { 234 219 app.auth.delete_tokens_for_user(user.id) or { 235 220 eprintln('failed to yeet tokens for ${user.id}') 236 - return ctx.redirect('/login') 237 221 } 238 222 } else { 239 223 eprintln('failed to get user for token for full_logout') ··· 250 234 path: '/' 251 235 ) 252 236 253 - return ctx.redirect('/login') 237 + return ctx.ok('logged out') 254 238 } 255 239 256 240 @['/api/user/set_nickname'; post] 257 241 fn (mut app App) api_user_set_nickname(mut ctx Context, nickname string) veb.Result { 258 242 user := app.whoami(mut ctx) or { 259 - ctx.error('you are not logged in!') 260 - return ctx.redirect('/login') 243 + return ctx.unauthorized(not_logged_in_msg) 261 244 } 262 245 263 246 mut clean_nickname := ?string(nickname.trim_space()) ··· 267 250 268 251 // validate 269 252 if clean_nickname != none && !app.validators.nickname.validate(clean_nickname or { '' }) { 270 - ctx.error('invalid nickname') 271 - return ctx.redirect('/settings') 253 + return ctx.server_error('invalid nickname') 272 254 } 273 255 274 256 if !app.set_nickname(user.id, clean_nickname) { 275 257 eprintln('failed to update nickname for ${user} (${user.nickname} -> ${clean_nickname})') 276 - return ctx.redirect('/settings') 258 + return ctx.server_error('failed to update nickname') 277 259 } 278 260 279 - return ctx.redirect('/settings') 261 + return ctx.ok('updated nickname') 280 262 } 281 263 282 264 @['/api/user/set_muted'; post] 283 265 fn (mut app App) api_user_set_muted(mut ctx Context, id int, muted bool) veb.Result { 284 266 user := app.whoami(mut ctx) or { 285 - ctx.error('you are not logged in!') 286 - return ctx.redirect('/login') 267 + return ctx.unauthorized(not_logged_in_msg) 287 268 } 288 269 289 270 to_mute := app.get_user_by_id(id) or { 290 - ctx.error('no such user') 291 - return ctx.redirect('/') 271 + return ctx.server_error('no such user') 292 272 } 293 273 294 274 if user.admin { 295 275 if !app.set_muted(to_mute.id, muted) { 296 - ctx.error('failed to change mute status') 297 - return ctx.redirect('/user/${to_mute.username}') 276 + return ctx.server_error('failed to change mute status') 298 277 } 299 - return ctx.redirect('/user/${to_mute.username}') 278 + return ctx.ok('muted user') 300 279 } else { 301 - ctx.error('insufficient permissions!') 302 280 eprintln('insufficient perms to update mute status for ${to_mute} (${to_mute.muted} -> ${muted})') 303 - return ctx.redirect('/user/${to_mute.username}') 281 + return ctx.unauthorized('insufficient permissions') 304 282 } 305 283 } 306 284 307 285 @['/api/user/set_automated'; post] 308 286 fn (mut app App) api_user_set_automated(mut ctx Context, is_automated bool) veb.Result { 309 287 user := app.whoami(mut ctx) or { 310 - ctx.error('you are not logged in!') 311 - return ctx.redirect('/login') 288 + return ctx.unauthorized(not_logged_in_msg) 312 289 } 313 290 314 291 if !app.set_automated(user.id, is_automated) { 315 - ctx.error('failed to set automated status.') 292 + return ctx.server_error('failed to set automated status.') 316 293 } 317 294 318 - return ctx.redirect('/settings') 295 + if is_automated { 296 + return ctx.ok('you\'re now a bot! :D') 297 + } else { 298 + return ctx.ok('you\'re no longer a bot :(') 299 + } 319 300 } 320 301 321 302 @['/api/user/set_theme'; post] 322 303 fn (mut app App) api_user_set_theme(mut ctx Context, url string) veb.Result { 323 304 if !app.config.instance.allow_changing_theme { 324 - ctx.error('this instance disallows changing themes :(') 325 - return ctx.redirect('/settings') 305 + return ctx.server_error('this instance disallows changing themes :(') 326 306 } 327 307 328 308 user := app.whoami(mut ctx) or { 329 - ctx.error('you are not logged in!') 330 - return ctx.redirect('/login') 309 + return ctx.unauthorized(not_logged_in_msg) 331 310 } 332 311 333 312 mut theme := ?string(none) ··· 338 317 } 339 318 340 319 if !app.set_theme(user.id, theme) { 341 - ctx.error('failed to change theme') 342 - return ctx.redirect('/settings') 320 + return ctx.server_error('failed to change theme') 343 321 } 344 322 345 - return ctx.redirect('/settings') 323 + return ctx.ok('theme updated') 346 324 } 347 325 348 326 @['/api/user/set_css'; post] 349 327 fn (mut app App) api_user_set_css(mut ctx Context, css string) veb.Result { 350 328 if !app.config.instance.allow_changing_theme { 351 - ctx.error('this instance disallows changing themes :(') 352 - return ctx.redirect('/settings') 329 + return ctx.server_error('this instance disallows changing themes :(') 353 330 } 354 331 355 332 user := app.whoami(mut ctx) or { 356 - ctx.error('you are not logged in!') 357 - return ctx.redirect('/login') 333 + return ctx.unauthorized(not_logged_in_msg) 358 334 } 359 335 360 336 c := if css.trim_space() == '' { app.config.instance.default_css } else { css.trim_space() } 361 337 362 338 if !app.set_css(user.id, c) { 363 - ctx.error('failed to change css') 364 - return ctx.redirect('/settings') 339 + return ctx.server_error('failed to change css') 365 340 } 366 341 367 - return ctx.redirect('/settings') 342 + return ctx.ok('css updated') 368 343 } 369 344 370 345 @['/api/user/set_pronouns'; post] 371 346 fn (mut app App) api_user_set_pronouns(mut ctx Context, pronouns string) veb.Result { 372 347 user := app.whoami(mut ctx) or { 373 - ctx.error('you are not logged in!') 374 - return ctx.redirect('/login') 348 + return ctx.unauthorized(not_logged_in_msg) 375 349 } 376 350 377 351 clean_pronouns := pronouns.trim_space() 378 352 if !app.validators.pronouns.validate(clean_pronouns) { 379 - ctx.error('invalid pronouns') 380 - return ctx.redirect('/settings') 353 + return ctx.server_error('invalid pronouns') 381 354 } 382 355 383 356 if !app.set_pronouns(user.id, clean_pronouns) { 384 - ctx.error('failed to change pronouns') 385 - return ctx.redirect('/settings') 357 + return ctx.server_error('failed to change pronouns') 386 358 } 387 359 388 - return ctx.redirect('/settings') 360 + return ctx.ok('pronouns updated') 389 361 } 390 362 391 363 @['/api/user/set_bio'; post] 392 364 fn (mut app App) api_user_set_bio(mut ctx Context, bio string) veb.Result { 393 365 user := app.whoami(mut ctx) or { 394 - ctx.error('you are not logged in!') 395 - return ctx.redirect('/login') 366 + return ctx.unauthorized(not_logged_in_msg) 396 367 } 397 368 398 369 clean_bio := bio.trim_space() 399 370 if !app.validators.user_bio.validate(clean_bio) { 400 - ctx.error('invalid bio') 401 - return ctx.redirect('/settings') 371 + return ctx.server_error('invalid bio') 402 372 } 403 373 404 374 if !app.set_bio(user.id, clean_bio) { 405 375 eprintln('failed to update bio for ${user} (${user.bio} -> ${clean_bio})') 406 - return ctx.redirect('/settings') 376 + return ctx.server_error('failed to update bio') 407 377 } 408 378 409 - return ctx.redirect('/settings') 379 + return ctx.ok('bio updated') 410 380 } 411 381 412 - @['/api/user/get_name'] 382 + @['/api/user/get_name'; get] 413 383 fn (mut app App) api_user_get_name(mut ctx Context, username string) veb.Result { 384 + if !app.config.instance.public_data { 385 + return ctx.server_error('no such error') 386 + } 414 387 user := app.get_user_by_name(username) or { return ctx.server_error('no such user') } 415 388 return ctx.text(user.get_name()) 416 389 } 417 390 418 - @['/api/user/delete'] 391 + @['/api/user/delete'; post] 419 392 fn (mut app App) api_user_delete(mut ctx Context, id int) veb.Result { 420 393 user := app.whoami(mut ctx) or { 421 - ctx.error('you are not logged in!') 422 - return ctx.redirect('/login') 394 + return ctx.unauthorized(not_logged_in_msg) 423 395 } 424 396 425 - println('attempting to delete ${id} as ${user.id}') 397 + if user.admin || user.id == id { 398 + println('attempting to delete ${id} as ${user.id}') 426 399 427 - if user.admin || user.id == id { 428 400 // yeet 429 401 if !app.delete_user(user.id) { 430 - ctx.error('failed to delete user: ${id}') 431 - return ctx.redirect('/') 402 + return ctx.server_error('failed to delete user: ${id}') 432 403 } 433 404 434 405 app.auth.delete_tokens_for_user(id) or { ··· 445 416 ) 446 417 } 447 418 println('deleted user ${id}') 419 + return ctx.ok('user deleted') 448 420 } else { 449 - ctx.error('be nice. deleting other users is off-limits.') 421 + return ctx.unauthorized('be nice. deleting other users is off-limits.') 450 422 } 451 - 452 - return ctx.redirect('/') 453 423 } 454 424 455 425 @['/api/user/search'; get] 456 426 fn (mut app App) api_user_search(mut ctx Context, query string, limit int, offset int) veb.Result { 457 - _ := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') } 427 + _ := app.whoami(mut ctx) or { return ctx.unauthorized(not_logged_in_msg) } 458 428 if limit >= search_hard_limit { 459 - return ctx.text('limit exceeds hard limit (${search_hard_limit})') 429 + return ctx.server_error('limit exceeds hard limit (${search_hard_limit})') 460 430 } 461 431 users := app.search_for_users(query, limit, offset) 462 432 return ctx.json[[]User](users) ··· 464 434 465 435 @['/api/user/whoami'; get] 466 436 fn (mut app App) api_user_whoami(mut ctx Context) veb.Result { 467 - user := app.whoami(mut ctx) or { return ctx.text('not logged in') } 437 + user := app.whoami(mut ctx) or { 438 + return ctx.unauthorized(not_logged_in_msg) 439 + } 468 440 return ctx.text(user.username) 469 441 } 470 442 471 443 /// user/notification /// 472 444 473 - @['/api/user/notification/clear'] 445 + @['/api/user/notification/clear'; post] 474 446 fn (mut app App) api_user_notification_clear(mut ctx Context, id int) veb.Result { 475 447 user := app.whoami(mut ctx) or { 476 - ctx.error('you are not logged in!') 477 - return ctx.redirect('/login') 448 + return ctx.unauthorized(not_logged_in_msg) 478 449 } 479 450 480 451 if notification := app.get_notification_by_id(id) { 481 452 if notification.user_id != user.id { 482 - ctx.error('no such notification for user') 483 - return ctx.redirect('/inbox') 484 - } else { 485 - if !app.delete_notification(id) { 486 - ctx.error('failed to delete notification') 487 - return ctx.redirect('/inbox') 488 - } 453 + return ctx.server_error('no such notification for user') 454 + } else if !app.delete_notification(id) { 455 + return ctx.server_error('failed to delete notification') 489 456 } 490 457 } else { 491 - ctx.error('no such notification for user') 458 + return ctx.server_error('no such notification for user') 492 459 } 493 460 494 - return ctx.redirect('/inbox') 461 + return ctx.ok('cleared notification') 495 462 } 496 463 497 - @['/api/user/notification/clear_all'] 464 + @['/api/user/notification/clear_all'; post] 498 465 fn (mut app App) api_user_notification_clear_all(mut ctx Context) veb.Result { 499 466 user := app.whoami(mut ctx) or { 500 - ctx.error('you are not logged in!') 501 - return ctx.redirect('/login') 467 + return ctx.unauthorized(not_logged_in_msg) 502 468 } 503 469 if !app.delete_notifications_for_user(user.id) { 504 - ctx.error('failed to delete notifications') 505 - return ctx.redirect('/inbox') 470 + return ctx.server_error('failed to delete notifications') 506 471 } 507 - return ctx.redirect('/inbox') 472 + return ctx.ok('cleared notifications') 508 473 } 509 474 510 475 ////// post ////// ··· 512 477 @['/api/post/new_post'; post] 513 478 fn (mut app App) api_post_new_post(mut ctx Context, replying_to int, title string, body string) veb.Result { 514 479 user := app.whoami(mut ctx) or { 515 - ctx.error('not logged in!') 516 - return ctx.redirect('/login') 480 + return ctx.unauthorized(not_logged_in_msg) 517 481 } 518 482 519 483 if user.muted { 520 - ctx.error('you are muted!') 521 - return ctx.redirect('/post/new') 484 + return ctx.server_error('you are muted!') 522 485 } 523 486 524 487 // validate title 525 488 if !app.validators.post_title.validate(title) { 526 - ctx.error('invalid title') 527 - return ctx.redirect('/post/new') 489 + return ctx.server_error('invalid title') 528 490 } 529 491 530 492 // validate body 531 493 if !app.validators.post_body.validate(body) { 532 - ctx.error('invalid body') 533 - return ctx.redirect('/post/new') 494 + return ctx.server_error('invalid body') 534 495 } 535 496 536 497 nsfw := 'nsfw' in ctx.form 537 498 if nsfw && !app.config.post.allow_nsfw { 538 - ctx.error('nsfw posts are not allowed on this instance') 539 - return ctx.redirect('/post/new') 499 + return ctx.server_error('nsfw posts are not allowed on this instance') 540 500 } 541 501 542 502 mut post := Post{ ··· 549 509 if replying_to != 0 { 550 510 // check if replying post exists 551 511 app.get_post_by_id(replying_to) or { 552 - ctx.error('the post you are trying to reply to does not exist') 553 - return ctx.redirect('/post/new') 512 + return ctx.server_error('the post you are trying to reply to does not exist') 554 513 } 555 514 post.replying_to = replying_to 556 515 } 557 516 558 517 if !app.add_post(post) { 559 - ctx.error('failed to post!') 560 518 println('failed to post: ${post} from user ${user.id}') 561 - return ctx.redirect('/post/new') 519 + return ctx.server_error('failed to post') 562 520 } 563 521 522 + //TODO: Can I not just get the ID directly?? This method feels dicey at best. 564 523 // find the post's id to process mentions with 565 524 if x := app.get_post_by_author_and_timestamp(user.id, post.posted_at) { 566 525 app.process_post_mentions(x) 567 - return ctx.redirect('/post/${x.id}') 526 + return ctx.ok('posted. id=${x.id}') 568 527 } else { 569 - ctx.error('failed to get_post_by_timestamp_and_author for ${post}') 570 - return ctx.redirect('/me') 528 + eprintln('api_post_new_post: get_post_by_timestamp_and_author failed for ${post}') 529 + return ctx.server_error('failed to get post ID, this error should never happen') 571 530 } 572 531 } 573 532 574 533 @['/api/post/delete'; post] 575 534 fn (mut app App) api_post_delete(mut ctx Context, id int) veb.Result { 576 535 user := app.whoami(mut ctx) or { 577 - ctx.error('not logged in!') 578 - return ctx.redirect('/login') 536 + return ctx.unauthorized(not_logged_in_msg) 579 537 } 580 538 581 539 post := app.get_post_by_id(id) or { 582 - ctx.error('post does not exist') 583 - return ctx.redirect('/') 540 + return ctx.server_error('post does not exist') 584 541 } 585 542 586 543 if user.admin || user.id == post.author_id { 587 544 if !app.delete_post(post.id) { 588 - ctx.error('failed to delete post') 589 - eprintln('failed to delete post: ${id}') 590 - return ctx.redirect('/') 545 + eprintln('api_post_delete: failed to delete post: ${id}') 546 + return ctx.server_error('failed to delete post') 591 547 } 592 548 println('deleted post: ${id}') 593 - return ctx.redirect('/') 549 + return ctx.ok('post deleted') 594 550 } else { 595 - ctx.error('insufficient permissions!') 596 551 eprintln('insufficient perms to delete post: ${id} (${user.id})') 597 - return ctx.redirect('/') 552 + return ctx.unauthorized('insufficient permissions') 598 553 } 599 554 } 600 555 601 - @['/api/post/like'] 556 + @['/api/post/like'; post] 602 557 fn (mut app App) api_post_like(mut ctx Context, id int) veb.Result { 603 - user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') } 558 + user := app.whoami(mut ctx) or { 559 + return ctx.unauthorized(not_logged_in_msg) 560 + } 604 561 605 - post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') } 562 + post := app.get_post_by_id(id) or { 563 + return ctx.server_error('post does not exist') 564 + } 606 565 607 566 if app.does_user_like_post(user.id, post.id) { 608 567 if !app.unlike_post(post.id, user.id) { ··· 631 590 } 632 591 } 633 592 634 - @['/api/post/dislike'] 593 + @['/api/post/dislike'; post] 635 594 fn (mut app App) api_post_dislike(mut ctx Context, id int) veb.Result { 636 - user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') } 595 + user := app.whoami(mut ctx) or { 596 + return ctx.unauthorized(not_logged_in_msg) 597 + } 637 598 638 - post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') } 599 + post := app.get_post_by_id(id) or { 600 + return ctx.server_error('post does not exist') 601 + } 639 602 640 603 if app.does_user_dislike_post(user.id, post.id) { 641 604 if !app.unlike_post(post.id, user.id) { ··· 664 627 } 665 628 } 666 629 667 - @['/api/post/save'] 630 + @['/api/post/save'; post] 668 631 fn (mut app App) api_post_save(mut ctx Context, id int) veb.Result { 669 - user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') } 632 + user := app.whoami(mut ctx) or { 633 + return ctx.unauthorized(not_logged_in_msg) 634 + } 670 635 671 636 if app.get_post_by_id(id) != none { 672 637 if app.toggle_save_post(user.id, id) { ··· 679 644 } 680 645 } 681 646 682 - @['/api/post/save_for_later'] 647 + @['/api/post/save_for_later'; post] 683 648 fn (mut app App) api_post_save_for_later(mut ctx Context, id int) veb.Result { 684 - user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') } 649 + user := app.whoami(mut ctx) or { 650 + return ctx.unauthorized(not_logged_in_msg) 651 + } 685 652 686 653 if app.get_post_by_id(id) != none { 687 654 if app.toggle_save_for_later_post(user.id, id) { ··· 694 661 } 695 662 } 696 663 697 - @['/api/post/get_title'] 664 + @['/api/post/get_title'; get] 698 665 fn (mut app App) api_post_get_title(mut ctx Context, id int) veb.Result { 699 666 if !app.config.instance.public_data { 700 - _ := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') } 667 + _ := app.whoami(mut ctx) or { return ctx.unauthorized(not_logged_in_msg) } 701 668 } 702 669 post := app.get_post_by_id(id) or { return ctx.server_error('no such post') } 703 670 return ctx.text(post.title) ··· 706 673 @['/api/post/edit'; post] 707 674 fn (mut app App) api_post_edit(mut ctx Context, id int, title string, body string) veb.Result { 708 675 user := app.whoami(mut ctx) or { 709 - ctx.error('not logged in!') 710 - return ctx.redirect('/login') 676 + return ctx.unauthorized(not_logged_in_msg) 711 677 } 712 678 post := app.get_post_by_id(id) or { 713 - ctx.error('no such post') 714 - return ctx.redirect('/') 679 + return ctx.server_error('no such post') 715 680 } 716 681 if post.author_id != user.id { 717 - ctx.error('insufficient permissions') 718 - return ctx.redirect('/') 682 + return ctx.unauthorized('insufficient permissions') 719 683 } 720 684 721 - if !app.update_post(id, title, body) { 685 + nsfw := if 'nsfw' in ctx.form { 686 + app.config.post.allow_nsfw 687 + } else { 688 + post.nsfw 689 + } 690 + 691 + if !app.update_post(id, title, body, nsfw) { 722 692 eprintln('failed to update post') 723 - ctx.error('failed to update post') 724 - return ctx.redirect('/') 693 + return ctx.server_error('failed to update post') 725 694 } 726 695 727 - return ctx.redirect('/post/${id}') 696 + return ctx.ok('posted edited') 728 697 } 729 698 730 699 @['/api/post/pin'; post] 731 700 fn (mut app App) api_post_pin(mut ctx Context, id int) veb.Result { 732 701 user := app.whoami(mut ctx) or { 733 - ctx.error('not logged in!') 734 - return ctx.redirect('/login') 702 + return ctx.unauthorized(not_logged_in_msg) 735 703 } 736 704 737 705 if user.admin { 738 706 if !app.pin_post(id) { 739 707 eprintln('failed to pin post: ${id}') 740 - ctx.error('failed to pin post') 741 - return ctx.redirect('/post/${id}') 708 + return ctx.server_error('failed to pin post') 742 709 } 743 - return ctx.redirect('/post/${id}') 710 + return ctx.ok('post pinned') 744 711 } else { 745 - ctx.error('insufficient permissions!') 746 712 eprintln('insufficient perms to pin post: ${id} (${user.id})') 747 - return ctx.redirect('/') 713 + return ctx.unauthorized('insufficient permissions') 748 714 } 749 715 } 750 716 751 717 @['/api/post/get/<id>'; get] 752 718 fn (mut app App) api_post_get_post(mut ctx Context, id int) veb.Result { 753 719 if !app.config.instance.public_data { 754 - _ := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') } 720 + _ := app.whoami(mut ctx) or { return ctx.unauthorized(not_logged_in_msg) } 755 721 } 756 722 post := app.get_post_by_id(id) or { return ctx.text('no such post') } 757 723 return ctx.json[Post](post) ··· 759 725 760 726 @['/api/post/search'; get] 761 727 fn (mut app App) api_post_search(mut ctx Context, query string, limit int, offset int) veb.Result { 762 - _ := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') } 728 + _ := app.whoami(mut ctx) or { return ctx.unauthorized(not_logged_in_msg) } 763 729 if limit >= search_hard_limit { 764 730 return ctx.text('limit exceeds hard limit (${search_hard_limit})') 765 731 } ··· 772 738 @['/api/site/set_motd'; post] 773 739 fn (mut app App) api_site_set_motd(mut ctx Context, motd string) veb.Result { 774 740 user := app.whoami(mut ctx) or { 775 - ctx.error('not logged in!') 776 - return ctx.redirect('/login') 741 + return ctx.unauthorized(not_logged_in_msg) 777 742 } 778 743 779 744 if user.admin { 780 745 if !app.set_motd(motd) { 781 - ctx.error('failed to set motd') 782 746 eprintln('failed to set motd: ${motd}') 783 - return ctx.redirect('/') 747 + return ctx.server_error('failed to set motd') 784 748 } 785 749 println('set motd to: ${motd}') 786 - return ctx.redirect('/') 750 + return ctx.ok('motd updated') 787 751 } else { 788 - ctx.error('insufficient permissions!') 789 752 eprintln('insufficient perms to set motd to: ${motd} (${user.id})') 790 - return ctx.redirect('/') 753 + return ctx.unauthorized('insufficient permissions') 791 754 } 792 755 }