+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
- https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html#other-examples-of-safe-prepared-statements
33
- https://cheatsheetseries.owasp.org/cheatsheets/Query_Parameterization_Cheat_Sheet.html#using-net-built-in-feature
34
- https://www.slideshare.net/slideshow/sql-injection-myths-and-fallacies/3729931#3
···
32
- https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html#other-examples-of-safe-prepared-statements
33
- https://cheatsheetseries.owasp.org/cheatsheets/Query_Parameterization_Cheat_Sheet.html#using-net-built-in-feature
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
required
35
>
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
42
<input type="submit" value="register">
43
</form>
44
@end
45
</div>
46
47
+
@include 'partial/footer.html'
+35
-13
src/webapp/api.v
+35
-13
src/webapp/api.v
···
2
3
import veb
4
import auth
5
-
import entity { Like, LikeCache, Post, Site, User, Notification }
6
import database { PostSearchResult }
7
8
// search_hard_limit is the maximum limit for a search query, used to prevent
9
// people from requesting searches with huge limits and straining the SQL server
10
-
pub const search_hard_limit := 50
11
12
////// user //////
13
14
@['/api/user/register'; post]
15
fn (mut app App) api_user_register(mut ctx Context, username string, password string) veb.Result {
16
if app.get_user_by_name(username) != none {
17
ctx.error('username taken')
18
return ctx.redirect('/register')
···
42
}
43
44
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
-
)
50
token := app.auth.add_token(x.id, ctx.ip()) or {
51
eprintln(err)
52
ctx.error('api_user_register: could not create token for user with id ${x.id}')
···
399
400
@['/api/user/whoami'; get]
401
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
-
}
405
return ctx.text(user.username)
406
}
407
···
677
678
@['/api/post/get/<id>'; get]
679
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
-
}
683
return ctx.json[Post](post)
684
}
685
···
2
3
import veb
4
import auth
5
+
import entity { Like, Post, User }
6
import database { PostSearchResult }
7
+
import net.http
8
+
import json
9
10
// search_hard_limit is the maximum limit for a search query, used to prevent
11
// people from requesting searches with huge limits and straining the SQL server
12
+
pub const search_hard_limit = 50
13
14
////// user //////
15
16
+
struct HcaptchaResponse {
17
+
pub:
18
+
success bool
19
+
error_codes []string @[json: 'error-codes']
20
+
}
21
+
22
@['/api/user/register'; post]
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
+
45
if app.get_user_by_name(username) != none {
46
ctx.error('username taken')
47
return ctx.redirect('/register')
···
71
}
72
73
if x := app.new_user(user) {
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()))
76
token := app.auth.add_token(x.id, ctx.ip()) or {
77
eprintln(err)
78
ctx.error('api_user_register: could not create token for user with id ${x.id}')
···
425
426
@['/api/user/whoami'; get]
427
fn (mut app App) api_user_whoami(mut ctx Context) veb.Result {
428
+
user := app.whoami(mut ctx) or { return ctx.text('not logged in') }
429
return ctx.text(user.username)
430
}
431
···
701
702
@['/api/post/get/<id>'; get]
703
fn (mut app App) api_post_get_post(mut ctx Context, id int) veb.Result {
704
+
post := app.get_post_by_id(id) or { return ctx.text('no such post') }
705
return ctx.json[Post](post)
706
}
707
+11
src/webapp/config.v
+11
src/webapp/config.v
···
28
password string
29
db string
30
}
31
post struct {
32
pub mut:
33
title_min_len int
···
86
config.postgres.user = loaded_postgres.get('user').to_str()
87
config.postgres.password = loaded_postgres.get('password').to_str()
88
config.postgres.db = loaded_postgres.get('db').to_str()
89
90
loaded_post := loaded.get('post')
91
config.post.title_min_len = loaded_post.get('title_min_len').to_int()
···
28
password string
29
db string
30
}
31
+
hcaptcha struct {
32
+
pub mut:
33
+
enabled bool
34
+
secret string
35
+
site_key string
36
+
}
37
post struct {
38
pub mut:
39
title_min_len int
···
92
config.postgres.user = loaded_postgres.get('user').to_str()
93
config.postgres.password = loaded_postgres.get('password').to_str()
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()
100
101
loaded_post := loaded.get('post')
102
config.post.title_min_len = loaded_post.get('title_min_len').to_int()