a mini social media app for small communities

hcaptcha

+3
.editorconfig
··· 6 6 7 7 [*.v] 8 8 indent_style = tab 9 + 10 + [*.{html,css,js}] 11 + indent_style = tab
+17
compose.yml
··· 1 + version: "3" 2 + volumes: 3 + beep-data: 4 + beep-data-export: 5 + services: 6 + beep-database: 7 + image: docker.io/postgres:15-alpine 8 + container_name: beep-database 9 + ports: 10 + - 5432:5432 11 + environment: 12 + - POSTGRES_DB=beep 13 + - POSTGRES_USER=beep 14 + - POSTGRES_PASSWORD=beep 15 + volumes: 16 + - beep-data:/var/lib/postgresql/data 17 + - beep-data-export:/export
+6
config.maple
··· 27 27 db = 'beep' 28 28 } 29 29 30 + hcaptcha = { 31 + enabled = false 32 + secret = '' 33 + site_key = '' 34 + } 35 + 30 36 post = { 31 37 title_min_len = 1 32 38 title_max_len = 50
+10
doc/resources.md
··· 32 32 - https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html#other-examples-of-safe-prepared-statements 33 33 - https://cheatsheetseries.owasp.org/cheatsheets/Query_Parameterization_Cheat_Sheet.html#using-net-built-in-feature 34 34 - https://www.slideshare.net/slideshow/sql-injection-myths-and-fallacies/3729931#3 35 + 36 + ## misc 37 + 38 + - https://stackoverflow.blog/2021/12/28/what-i-wish-i-had-known-about-single-page-applications/ 39 + - i thought about turning beep into a single page application (spa), 40 + then done a bit of research. this blog post pointed out a variety of 41 + problems that the author had with their spa, and many of those problems 42 + would be problems for beep too. 43 + - tl;dr: this blog post gave me the warnings about an spa before i 44 + wasted my time implementing it on beep.
+1 -1
src/main.v
··· 71 71 log: true 72 72 ) 73 73 74 - defer { app.db.close() } 74 + defer { app.db.close() or { panic(err) } } 75 75 76 76 // add authenticator 77 77 app.auth = auth.new(app.db)
+6 -1
src/templates/register.html
··· 34 34 required 35 35 > 36 36 <br> 37 + @if app.config.hcaptcha.enabled 38 + <div class="h-captcha" data-sitekey="@{app.config.hcaptcha.site_key}"></div> 39 + <script src="https://js.hcaptcha.com/1/api.js" async defer></script> 40 + <br> 41 + @end 37 42 <input type="submit" value="register"> 38 43 </form> 39 44 @end 40 45 </div> 41 46 42 - @include 'partial/footer.html' 47 + @include 'partial/footer.html'
+35 -13
src/webapp/api.v
··· 2 2 3 3 import veb 4 4 import auth 5 - import entity { Like, LikeCache, Post, Site, User, Notification } 5 + import entity { Like, Post, User } 6 6 import database { PostSearchResult } 7 + import net.http 8 + import json 7 9 8 10 // search_hard_limit is the maximum limit for a search query, used to prevent 9 11 // people from requesting searches with huge limits and straining the SQL server 10 - pub const search_hard_limit := 50 12 + pub const search_hard_limit = 50 11 13 12 14 ////// user ////// 13 15 16 + struct HcaptchaResponse { 17 + pub: 18 + success bool 19 + error_codes []string @[json: 'error-codes'] 20 + } 21 + 14 22 @['/api/user/register'; post] 15 23 fn (mut app App) api_user_register(mut ctx Context, username string, password string) veb.Result { 24 + // before doing *anything*, check the captchas 25 + if app.config.hcaptcha.enabled { 26 + token := ctx.form['h-captcha-response'] 27 + response := http.post('https://api.hcaptcha.com/siteverify', '{ 28 + "secret": "${app.config.hcaptcha.site_key}", 29 + "remoteip": "${ctx.ip()}", 30 + "response": "${token}" 31 + }') or { 32 + ctx.error('failed to post hcaptcha response: ${err}') 33 + return ctx.redirect('/register') 34 + } 35 + data := json.decode(HcaptchaResponse, response.body) or { 36 + ctx.error('failed to decode hcaptcha response: ${err}') 37 + return ctx.redirect('/register') 38 + } 39 + if !data.success { 40 + ctx.error('failed to verify hcaptcha: ${data}') 41 + return ctx.redirect('/register') 42 + } 43 + } 44 + 16 45 if app.get_user_by_name(username) != none { 17 46 ctx.error('username taken') 18 47 return ctx.redirect('/register') ··· 42 71 } 43 72 44 73 if x := app.new_user(user) { 45 - app.send_notification_to( 46 - x.id, 47 - app.config.welcome.summary.replace('%s', x.get_name()), 48 - app.config.welcome.body.replace('%s', x.get_name()) 49 - ) 74 + app.send_notification_to(x.id, app.config.welcome.summary.replace('%s', x.get_name()), 75 + app.config.welcome.body.replace('%s', x.get_name())) 50 76 token := app.auth.add_token(x.id, ctx.ip()) or { 51 77 eprintln(err) 52 78 ctx.error('api_user_register: could not create token for user with id ${x.id}') ··· 399 425 400 426 @['/api/user/whoami'; get] 401 427 fn (mut app App) api_user_whoami(mut ctx Context) veb.Result { 402 - user := app.whoami(mut ctx) or { 403 - return ctx.text('not logged in') 404 - } 428 + user := app.whoami(mut ctx) or { return ctx.text('not logged in') } 405 429 return ctx.text(user.username) 406 430 } 407 431 ··· 677 701 678 702 @['/api/post/get/<id>'; get] 679 703 fn (mut app App) api_post_get_post(mut ctx Context, id int) veb.Result { 680 - post := app.get_post_by_id(id) or { 681 - return ctx.text('no such post') 682 - } 704 + post := app.get_post_by_id(id) or { return ctx.text('no such post') } 683 705 return ctx.json[Post](post) 684 706 } 685 707
+11
src/webapp/config.v
··· 28 28 password string 29 29 db string 30 30 } 31 + hcaptcha struct { 32 + pub mut: 33 + enabled bool 34 + secret string 35 + site_key string 36 + } 31 37 post struct { 32 38 pub mut: 33 39 title_min_len int ··· 86 92 config.postgres.user = loaded_postgres.get('user').to_str() 87 93 config.postgres.password = loaded_postgres.get('password').to_str() 88 94 config.postgres.db = loaded_postgres.get('db').to_str() 95 + 96 + loaded_hcaptcha := loaded.get('hcaptcha') 97 + config.hcaptcha.enabled = loaded_hcaptcha.get('enabled').to_bool() 98 + config.hcaptcha.secret = loaded_hcaptcha.get('secret').to_str() 99 + config.hcaptcha.site_key = loaded_hcaptcha.get('site_key').to_str() 89 100 90 101 loaded_post := loaded.get('post') 91 102 config.post.title_min_len = loaded_post.get('title_min_len').to_int()
+1 -1
v.mod
··· 1 1 Module { 2 2 name: 'beep' 3 - description: 'A self-hosted mini-blogger' 3 + description: 'a self-hosted mini-blogger' 4 4 version: '1.0.0' 5 5 license: 'MIT' 6 6 author: 'EmmaTheMartian'