+3
.editorconfig
+3
.editorconfig
+17
compose.yml
+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
+6
config.maple
+10
doc/resources.md
+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
+1
-1
src/main.v
+6
-1
src/templates/register.html
+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
+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
+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()