tangled
alpha
login
or
join now
slices.network
/
quickslice
60
fork
atom
Auto-indexing service and GraphQL API for AT Protocol Records
quickslice.slices.network/
atproto
gleam
graphql
60
fork
atom
overview
issues
7
pulls
2
pipelines
refactor: remove dead settings handler
chadtmiller.com
3 months ago
2e64e677
0972cb53
-400
1 changed file
expand all
collapse all
unified
split
server
src
handlers
settings.gleam
-400
server/src/handlers/settings.gleam
···
1
1
-
import admin_session as session
2
2
-
import database/repositories/actors
3
3
-
import database/repositories/config as config_repo
4
4
-
import database/repositories/lexicons
5
5
-
import database/repositories/records
6
6
-
import gleam/erlang/process
7
7
-
import gleam/http as gleam_http
8
8
-
import gleam/int
9
9
-
import gleam/list
10
10
-
import gleam/option
11
11
-
import gleam/string
12
12
-
import importer
13
13
-
import jetstream_consumer
14
14
-
import lib/oauth/did_cache
15
15
-
import logging
16
16
-
import simplifile
17
17
-
import sqlight
18
18
-
import wisp
19
19
-
import wisp_flash
20
20
-
import zip_helper
21
21
-
22
22
-
pub type Context {
23
23
-
Context(
24
24
-
db: sqlight.Connection,
25
25
-
jetstream_consumer: option.Option(
26
26
-
process.Subject(jetstream_consumer.ManagerMessage),
27
27
-
),
28
28
-
did_cache: process.Subject(did_cache.Message),
29
29
-
)
30
30
-
}
31
31
-
32
32
-
pub fn handle(req: wisp.Request, ctx: Context) -> wisp.Response {
33
33
-
// Get current user from session
34
34
-
let #(current_user, user_is_admin) = case
35
35
-
session.get_current_user(req, ctx.db, ctx.did_cache)
36
36
-
{
37
37
-
Ok(#(did, handle, _access_token)) -> {
38
38
-
let admin = config_repo.is_admin(ctx.db, did)
39
39
-
#(option.Some(#(did, handle)), admin)
40
40
-
}
41
41
-
Error(_) -> #(option.None, False)
42
42
-
}
43
43
-
44
44
-
// Require admin access for the entire settings page
45
45
-
case user_is_admin {
46
46
-
False -> {
47
47
-
logging.log(
48
48
-
logging.Warning,
49
49
-
"[settings] Non-admin user attempted to access settings page",
50
50
-
)
51
51
-
wisp.redirect("/")
52
52
-
}
53
53
-
True -> handle_admin_request(req, ctx, current_user)
54
54
-
}
55
55
-
}
56
56
-
57
57
-
fn handle_admin_request(
58
58
-
req: wisp.Request,
59
59
-
ctx: Context,
60
60
-
_current_user: option.Option(#(String, String)),
61
61
-
) -> wisp.Response {
62
62
-
case req.method {
63
63
-
gleam_http.Get -> {
64
64
-
// TODO: Migrate settings page to client SPA
65
65
-
wisp.html_response(
66
66
-
"<h1>Settings</h1><p>Settings page will be migrated to the client SPA</p>",
67
67
-
200,
68
68
-
)
69
69
-
}
70
70
-
gleam_http.Post -> {
71
71
-
// Handle form submission (domain authority or lexicons upload or reset)
72
72
-
use form_data <- wisp.require_form(req)
73
73
-
74
74
-
// Check if this is a reset action
75
75
-
case list.key_find(form_data.values, "action") {
76
76
-
Ok("reset") -> {
77
77
-
// Handle reset action (admin-only, already verified)
78
78
-
handle_reset(req, form_data, ctx)
79
79
-
}
80
80
-
_ -> {
81
81
-
// Check if this is a lexicons upload
82
82
-
case list.key_find(form_data.files, "lexicons_zip") {
83
83
-
Ok(uploaded_file) -> {
84
84
-
// Handle lexicons ZIP upload
85
85
-
handle_lexicons_upload(req, uploaded_file, ctx)
86
86
-
}
87
87
-
Error(_) -> {
88
88
-
// Not a file upload, check for domain_authority field
89
89
-
case list.key_find(form_data.values, "domain_authority") {
90
90
-
Ok(domain_authority) -> {
91
91
-
// Validate domain authority format
92
92
-
case validate_domain_authority(domain_authority) {
93
93
-
Ok(_) -> {
94
94
-
// Save domain_authority to database
95
95
-
case
96
96
-
config_repo.set(
97
97
-
ctx.db,
98
98
-
"domain_authority",
99
99
-
domain_authority,
100
100
-
)
101
101
-
{
102
102
-
Ok(_) -> {
103
103
-
wisp.redirect("/settings")
104
104
-
|> wisp_flash.set_flash(
105
105
-
req,
106
106
-
"success",
107
107
-
"Domain authority saved successfully",
108
108
-
)
109
109
-
}
110
110
-
Error(_) -> {
111
111
-
logging.log(
112
112
-
logging.Error,
113
113
-
"[settings] Failed to save domain_authority",
114
114
-
)
115
115
-
wisp.redirect("/settings")
116
116
-
|> wisp_flash.set_flash(
117
117
-
req,
118
118
-
"error",
119
119
-
"Failed to save domain authority",
120
120
-
)
121
121
-
}
122
122
-
}
123
123
-
}
124
124
-
Error(error_message) -> {
125
125
-
logging.log(
126
126
-
logging.Warning,
127
127
-
"[settings] Invalid domain authority: " <> error_message,
128
128
-
)
129
129
-
wisp.redirect("/settings")
130
130
-
|> wisp_flash.set_flash(req, "error", error_message)
131
131
-
}
132
132
-
}
133
133
-
}
134
134
-
Error(_) -> {
135
135
-
logging.log(
136
136
-
logging.Warning,
137
137
-
"[settings] No form data received",
138
138
-
)
139
139
-
wisp.redirect("/settings")
140
140
-
}
141
141
-
}
142
142
-
}
143
143
-
}
144
144
-
}
145
145
-
}
146
146
-
}
147
147
-
_ -> {
148
148
-
wisp.response(405)
149
149
-
|> wisp.set_header("content-type", "text/html")
150
150
-
|> wisp.set_body(wisp.Text("<h1>Method Not Allowed</h1>"))
151
151
-
}
152
152
-
}
153
153
-
}
154
154
-
155
155
-
fn handle_lexicons_upload(
156
156
-
req: wisp.Request,
157
157
-
uploaded_file: wisp.UploadedFile,
158
158
-
ctx: Context,
159
159
-
) -> wisp.Response {
160
160
-
logging.log(
161
161
-
logging.Info,
162
162
-
"[settings] Processing lexicons ZIP upload: " <> uploaded_file.file_name,
163
163
-
)
164
164
-
165
165
-
// Create temporary directory for extraction with random suffix
166
166
-
let temp_dir = "tmp/lexicon_upload_" <> wisp.random_string(16)
167
167
-
168
168
-
case simplifile.create_directory_all(temp_dir) {
169
169
-
Ok(_) -> {
170
170
-
logging.log(
171
171
-
logging.Info,
172
172
-
"[settings] Created temp directory: " <> temp_dir,
173
173
-
)
174
174
-
175
175
-
// Extract ZIP file to temp directory
176
176
-
case zip_helper.extract_zip(uploaded_file.path, temp_dir) {
177
177
-
Ok(_) -> {
178
178
-
logging.log(
179
179
-
logging.Info,
180
180
-
"[settings] Extracted ZIP file to: " <> temp_dir,
181
181
-
)
182
182
-
183
183
-
// Import lexicons from extracted directory
184
184
-
case importer.import_lexicons_from_directory(temp_dir, ctx.db) {
185
185
-
Ok(stats) -> {
186
186
-
// Clean up temp directory
187
187
-
let _ = simplifile.delete(temp_dir)
188
188
-
189
189
-
logging.log(
190
190
-
logging.Info,
191
191
-
"[settings] Lexicon import complete: "
192
192
-
<> int.to_string(stats.imported)
193
193
-
<> " imported, "
194
194
-
<> int.to_string(stats.failed)
195
195
-
<> " failed",
196
196
-
)
197
197
-
198
198
-
// Log any errors
199
199
-
case stats.errors {
200
200
-
[] -> Nil
201
201
-
errors -> {
202
202
-
list.each(errors, fn(err) {
203
203
-
logging.log(
204
204
-
logging.Warning,
205
205
-
"[settings] Import error: " <> err,
206
206
-
)
207
207
-
})
208
208
-
}
209
209
-
}
210
210
-
211
211
-
// Restart Jetstream consumer to pick up newly imported collections
212
212
-
let restart_status = case ctx.jetstream_consumer {
213
213
-
option.Some(consumer) -> {
214
214
-
logging.log(
215
215
-
logging.Info,
216
216
-
"[settings] Restarting Jetstream consumer with new lexicons...",
217
217
-
)
218
218
-
case jetstream_consumer.restart(consumer) {
219
219
-
Ok(_) -> {
220
220
-
logging.log(
221
221
-
logging.Info,
222
222
-
"[settings] Jetstream consumer restarted successfully",
223
223
-
)
224
224
-
"success"
225
225
-
}
226
226
-
Error(err) -> {
227
227
-
logging.log(
228
228
-
logging.Error,
229
229
-
"[settings] Failed to restart Jetstream consumer: "
230
230
-
<> err,
231
231
-
)
232
232
-
"failed"
233
233
-
}
234
234
-
}
235
235
-
}
236
236
-
option.None -> {
237
237
-
logging.log(
238
238
-
logging.Info,
239
239
-
"[settings] Jetstream consumer not running, skipping restart",
240
240
-
)
241
241
-
"not_running"
242
242
-
}
243
243
-
}
244
244
-
245
245
-
// Build success message with import stats and restart status
246
246
-
let base_message =
247
247
-
"Imported "
248
248
-
<> int.to_string(stats.imported)
249
249
-
<> " lexicon(s) successfully"
250
250
-
let message = case restart_status {
251
251
-
"success" -> base_message <> ". Jetstream consumer restarted."
252
252
-
"failed" ->
253
253
-
base_message
254
254
-
<> ". Warning: Jetstream consumer restart failed."
255
255
-
"not_running" -> base_message <> "."
256
256
-
_ -> base_message
257
257
-
}
258
258
-
259
259
-
let flash_kind = case restart_status {
260
260
-
"failed" -> "warning"
261
261
-
_ -> "success"
262
262
-
}
263
263
-
264
264
-
wisp.redirect("/settings")
265
265
-
|> wisp_flash.set_flash(req, flash_kind, message)
266
266
-
}
267
267
-
Error(err) -> {
268
268
-
// Clean up temp directory
269
269
-
let _ = simplifile.delete(temp_dir)
270
270
-
271
271
-
logging.log(
272
272
-
logging.Error,
273
273
-
"[settings] Failed to import lexicons: " <> err,
274
274
-
)
275
275
-
wisp.redirect("/settings")
276
276
-
|> wisp_flash.set_flash(
277
277
-
req,
278
278
-
"error",
279
279
-
"Failed to import lexicons: " <> err,
280
280
-
)
281
281
-
}
282
282
-
}
283
283
-
}
284
284
-
Error(err) -> {
285
285
-
// Clean up temp directory
286
286
-
let _ = simplifile.delete(temp_dir)
287
287
-
288
288
-
logging.log(
289
289
-
logging.Error,
290
290
-
"[settings] Failed to extract ZIP: " <> err,
291
291
-
)
292
292
-
wisp.redirect("/settings")
293
293
-
|> wisp_flash.set_flash(
294
294
-
req,
295
295
-
"error",
296
296
-
"Failed to extract ZIP file: " <> err,
297
297
-
)
298
298
-
}
299
299
-
}
300
300
-
}
301
301
-
Error(_) -> {
302
302
-
logging.log(logging.Error, "[settings] Failed to create temp directory")
303
303
-
wisp.redirect("/settings")
304
304
-
|> wisp_flash.set_flash(
305
305
-
req,
306
306
-
"error",
307
307
-
"Failed to create temporary directory for upload",
308
308
-
)
309
309
-
}
310
310
-
}
311
311
-
}
312
312
-
313
313
-
/// Validates domain authority format (e.g., "com.example")
314
314
-
fn validate_domain_authority(domain_authority: String) -> Result(Nil, String) {
315
315
-
// Check for http:// or https://
316
316
-
case
317
317
-
string.contains(domain_authority, "http://")
318
318
-
|| string.contains(domain_authority, "https://")
319
319
-
{
320
320
-
True ->
321
321
-
Error(
322
322
-
"Domain authority should not contain http:// or https:// (e.g., com.example)",
323
323
-
)
324
324
-
False -> {
325
325
-
// Check that it has exactly two parts separated by a dot
326
326
-
let parts = string.split(domain_authority, ".")
327
327
-
case list.length(parts) == 2 {
328
328
-
False ->
329
329
-
Error(
330
330
-
"Domain authority must have exactly two parts separated by a dot (e.g., com.example)",
331
331
-
)
332
332
-
True -> {
333
333
-
// Check that no parts are empty
334
334
-
case list.all(parts, fn(part) { string.length(part) > 0 }) {
335
335
-
False ->
336
336
-
Error(
337
337
-
"Domain authority parts cannot be empty (e.g., com.example)",
338
338
-
)
339
339
-
True -> Ok(Nil)
340
340
-
}
341
341
-
}
342
342
-
}
343
343
-
}
344
344
-
}
345
345
-
}
346
346
-
347
347
-
fn handle_reset(
348
348
-
req: wisp.Request,
349
349
-
form_data: wisp.FormData,
350
350
-
ctx: Context,
351
351
-
) -> wisp.Response {
352
352
-
// Admin access already verified by page-level check
353
353
-
// Verify confirmation
354
354
-
case list.key_find(form_data.values, "confirm") {
355
355
-
Ok("RESET") -> {
356
356
-
// Delete all data
357
357
-
let domain_result = config_repo.delete_domain_authority(ctx.db)
358
358
-
let lexicons_result = lexicons.delete_all(ctx.db)
359
359
-
let records_result = records.delete_all(ctx.db)
360
360
-
let actors_result = actors.delete_all(ctx.db)
361
361
-
362
362
-
case domain_result, lexicons_result, records_result, actors_result {
363
363
-
Ok(_), Ok(_), Ok(_), Ok(_) -> {
364
364
-
// Restart Jetstream consumer if it exists
365
365
-
let restart_message = case ctx.jetstream_consumer {
366
366
-
option.Some(consumer) -> {
367
367
-
case jetstream_consumer.restart(consumer) {
368
368
-
Ok(_) -> "All data has been reset successfully"
369
369
-
Error(_) ->
370
370
-
"Data reset completed (Jetstream consumer may need manual restart)"
371
371
-
}
372
372
-
}
373
373
-
option.None -> "All data has been reset successfully"
374
374
-
}
375
375
-
376
376
-
logging.log(logging.Info, "[settings] System reset completed")
377
377
-
wisp.redirect("/settings")
378
378
-
|> wisp_flash.set_flash(req, "success", restart_message)
379
379
-
}
380
380
-
_, _, _, _ -> {
381
381
-
logging.log(logging.Error, "[settings] Failed to reset some data")
382
382
-
wisp.redirect("/settings")
383
383
-
|> wisp_flash.set_flash(req, "error", "Failed to reset all data")
384
384
-
}
385
385
-
}
386
386
-
}
387
387
-
_ -> {
388
388
-
logging.log(
389
389
-
logging.Warning,
390
390
-
"[settings] Reset attempted without proper confirmation",
391
391
-
)
392
392
-
wisp.redirect("/settings")
393
393
-
|> wisp_flash.set_flash(
394
394
-
req,
395
395
-
"error",
396
396
-
"Confirmation failed: Please type RESET exactly",
397
397
-
)
398
398
-
}
399
399
-
}
400
400
-
}