+2
-2
doc/todo.md
+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
+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
+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
+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'
+87
-41
src/templates/settings.html
+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
-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>