@@ -23,7 +23,7 @@ use crate::{
23
23
extract:: { Json , Path } ,
24
24
hash_password,
25
25
middleware:: session:: { self , Session } ,
26
- openapi:: ApiErrorResponse ,
26
+ openapi:: { ApiErrorResponse , EmptyApiResponse } ,
27
27
ops, NameOrUlid , ServerContext ,
28
28
} ;
29
29
use axum:: { extract:: State , http:: StatusCode , routing, Extension , Router } ;
@@ -34,7 +34,7 @@ use charted_database::{
34
34
} ;
35
35
use charted_types:: {
36
36
payloads:: user:: { CreateUserPayload , PatchUserPayload } ,
37
- User ,
37
+ PGUser , SqliteUser , User ,
38
38
} ;
39
39
use diesel:: { backend:: Backend , ExpressionMethods , QueryDsl } ;
40
40
use eyre:: Context ;
@@ -282,6 +282,7 @@ pub async fn create_user(
282
282
283
283
let user = User {
284
284
verified_publisher : false ,
285
+ prefers_gravatar : false ,
285
286
gravatar_email : None ,
286
287
description : None ,
287
288
avatar_hash : None ,
@@ -386,17 +387,55 @@ pub async fn get_user(State(cx): State<ServerContext>, Path(id_or_name): Path<Na
386
387
get,
387
388
path = "/v1/users/@me" ,
388
389
operation_id = "getSelfUser" ,
389
- tags = [ "Users" ]
390
+ tags = [ "Users" ] ,
391
+ responses(
392
+ (
393
+ status = 200 ,
394
+ description = "A single user found" ,
395
+ body = api:: Response <User >,
396
+ content_type = "application/json"
397
+ ) ,
398
+ (
399
+ status = 4 XX ,
400
+ description = "Any occurrence when authentication fails" ,
401
+ body = ApiErrorResponse ,
402
+ content_type = "application/json"
403
+ )
404
+ )
390
405
) ]
391
406
pub async fn get_self ( Extension ( Session { user, .. } ) : Extension < Session > ) -> api:: Response < User > {
392
407
api:: ok ( StatusCode :: OK , user)
393
408
}
394
409
395
410
/// Patch metadata about the current user.
396
- #[ utoipa:: path( patch, path = "/v1/users/@me" , operation_id = "patchSelf" , tag = "Users" ) ]
411
+ #[ utoipa:: path(
412
+ patch,
413
+ path = "/v1/users/@me" ,
414
+ operation_id = "patchSelf" ,
415
+ tag = "Users" ,
416
+ request_body(
417
+ content_type = "application/json" ,
418
+ description = "Update payload for the `User` entity" ,
419
+ content = ref( "PatchUserPayload" )
420
+ ) ,
421
+ responses(
422
+ (
423
+ status = 204 ,
424
+ description = "Patch was successfully reflected" ,
425
+ body = EmptyApiResponse ,
426
+ content_type = "application/json"
427
+ ) ,
428
+ (
429
+ status = 4 XX ,
430
+ description = "Any occurrence when authentication fails or if the patch couldn't be reflected" ,
431
+ body = ApiErrorResponse ,
432
+ content_type = "application/json"
433
+ )
434
+ )
435
+ ) ]
397
436
pub async fn patch (
398
437
State ( cx) : State < ServerContext > ,
399
- Extension ( Session { user, .. } ) : Extension < Session > ,
438
+ Extension ( Session { mut user, .. } ) : Extension < Session > ,
400
439
Json ( PatchUserPayload {
401
440
prefers_gravatar,
402
441
gravatar_email,
@@ -407,41 +446,177 @@ pub async fn patch(
407
446
name,
408
447
} ) : Json < PatchUserPayload > ,
409
448
) -> api:: Result < ( ) > {
449
+ if let Some ( prefers_gravatar) = prefers_gravatar {
450
+ if user. prefers_gravatar != prefers_gravatar {
451
+ user. prefers_gravatar = prefers_gravatar;
452
+ }
453
+ }
454
+
455
+ if let Some ( gravatar_email) = gravatar_email. as_deref ( ) {
456
+ // if `old` == None, then update the description
457
+ // if `old` == Some(..) && `old` != `gravatar_email`, commit update
458
+ // if `old` == Some(..) && `old` == `""`, commit as `None`
459
+ let old = user. gravatar_email . as_deref ( ) ;
460
+ if old. is_none ( ) && !gravatar_email. is_empty ( ) {
461
+ user. gravatar_email = Some ( gravatar_email. to_owned ( ) ) ;
462
+ } else if let Some ( old) = old
463
+ && !old. is_empty ( )
464
+ && old != gravatar_email
465
+ {
466
+ user. gravatar_email = Some ( gravatar_email. to_owned ( ) ) ;
467
+ } else if gravatar_email. is_empty ( ) {
468
+ user. description = None ;
469
+ }
470
+ }
471
+
472
+ if let Some ( description) = description {
473
+ if description. len ( ) > 140 {
474
+ let len = description. len ( ) ;
475
+ return Err ( api:: err (
476
+ StatusCode :: NOT_ACCEPTABLE ,
477
+ (
478
+ api:: ErrorCode :: ValidationFailed ,
479
+ "expected `description` to be less than 140 characters" ,
480
+ json ! ( {
481
+ "expected" : 140 ,
482
+ "received" : {
483
+ "over" : len - 140 ,
484
+ "length" : len
485
+ }
486
+ } ) ,
487
+ ) ,
488
+ ) ) ;
489
+ }
490
+
491
+ // if `old` == None, then update the description
492
+ // if `old` == Some(..) && `old` != `descroption`, commit update
493
+ // if `old` == Some(..) && `old` == `""`, commit as `None`
494
+ let old = user. description . as_deref ( ) ;
495
+ if old. is_none ( ) {
496
+ user. description = Some ( description) ;
497
+ } else if let Some ( old) = old
498
+ && !old. is_empty ( )
499
+ && old != description
500
+ {
501
+ user. description = Some ( description) ;
502
+ } else if description. is_empty ( ) {
503
+ user. description = None ;
504
+ }
505
+ }
506
+
507
+ if let Some ( username) = username {
508
+ // We need to validate that the username isn't already taken, so we will get a
509
+ // temporary connection.
510
+ match ops:: db:: user:: get ( & cx, NameOrUlid :: Name ( username. clone ( ) ) ) . await {
511
+ Ok ( None ) => { }
512
+ Ok ( Some ( _) ) => {
513
+ return Err ( api:: err (
514
+ StatusCode :: CONFLICT ,
515
+ (
516
+ api:: ErrorCode :: EntityAlreadyExists ,
517
+ "user with username already exists" ,
518
+ json ! ( { "username" : & username} ) ,
519
+ ) ,
520
+ ) )
521
+ }
522
+
523
+ Err ( e) => return Err ( api:: system_failure ( e) ) ,
524
+ } ;
525
+
526
+ // In deserialization of the request body, it'll validate that
527
+ // the name is correct anyway, so it is ok to set it here without
528
+ // even more validation.
529
+ user. username = username;
530
+ }
531
+
532
+ if let Some ( password) = password. as_deref ( ) {
533
+ let authz = cx. authz . as_ref ( ) ;
534
+ if authz. downcast :: < charted_authz_local:: Backend > ( ) . is_none ( ) {
535
+ return Err ( api:: err (
536
+ StatusCode :: NOT_ACCEPTABLE ,
537
+ (
538
+ api:: ErrorCode :: InvalidBody ,
539
+ "`password` is only supported on the local authz backend" ,
540
+ ) ,
541
+ ) ) ;
542
+ }
543
+
544
+ if password. len ( ) < 8 {
545
+ return Err ( api:: err (
546
+ StatusCode :: NOT_ACCEPTABLE ,
547
+ (
548
+ api:: ErrorCode :: InvalidPassword ,
549
+ "`password` length was expected to be 8 characters or longer" ,
550
+ ) ,
551
+ ) ) ;
552
+ }
553
+
554
+ user. password = Some ( hash_password ( password) . map_err ( |_| api:: internal_server_error ( ) ) ?) ;
555
+ }
556
+
410
557
let mut conn = cx
411
558
. pool
412
559
. get ( )
413
560
. inspect_err ( |e| {
414
561
sentry:: capture_error ( e) ;
415
562
tracing:: error!( error = %e, "failed to establish database connection" ) ;
416
563
} )
417
- . map_err ( |x | api:: system_failure :: < eyre :: Report > ( x . into ( ) ) ) ?;
564
+ . map_err ( |_ | api:: internal_server_error ( ) ) ?;
418
565
419
- let _ : Result < ( ) , diesel :: result :: Error > = charted_database:: connection!( @raw conn {
566
+ charted_database:: connection!( @raw conn {
420
567
PostgreSQL ( conn) => conn. build_transaction( ) . run( |txn| {
421
568
use postgresql:: users:: { dsl, table} ;
422
569
423
- // We have to box this query since we are doing multiple conditions
424
- let mut update = diesel:: update( table. filter( dsl:: id. eq( user. id) ) ) . into_boxed:: <diesel:: pg:: Pg >( ) ;
425
-
426
- // `prefers_gravatar` != null; perform update
427
- if let Some ( prefers_gravatar) = prefers_gravatar {
428
- //update = update.set(dsl::prefers_gravatar.eq(prefers_gravatar));
429
- }
430
-
431
- todo!( )
570
+ diesel:: update( table. filter( dsl:: id. eq( user. id) ) )
571
+ . set( user. into_pg( ) )
572
+ . execute( txn)
573
+ . map( |_| ( ) )
432
574
} ) ;
433
575
434
576
SQLite ( conn) => conn. immediate_transaction( |txn| {
435
577
use sqlite:: users:: { dsl, table} ;
436
578
437
- let mut update = diesel:: update( table. filter( dsl:: id. eq( user. id) ) ) . into_boxed:: <diesel:: sqlite:: Sqlite >( ) ;
438
-
439
- todo!( )
579
+ diesel:: update( table. filter( dsl:: id. eq( user. id) ) )
580
+ . set( user. into_sqlite( ) )
581
+ . execute( txn)
582
+ . map( |_| ( ) )
440
583
} ) ;
441
- } ) ;
584
+ } )
585
+ . inspect_err ( |e| {
586
+ sentry:: capture_error ( e) ;
587
+ tracing:: error!( error = %e, "failed to update user" ) ;
588
+ } )
589
+ . map_err ( |_| api:: internal_server_error ( ) ) ?;
442
590
443
- todo ! ( )
591
+ Ok ( api :: no_content ( ) )
444
592
}
445
593
446
- #[ utoipa:: path( delete, path = "/v1/users/@me" , operation_id = "deleteSelf" , tag = "Users" ) ]
447
- pub async fn delete ( ) { }
594
+ #[ utoipa:: path(
595
+ delete,
596
+
597
+ path = "/v1/users/@me" ,
598
+ operation_id = "deleteSelf" ,
599
+ tag = "Users" ,
600
+ responses(
601
+ (
602
+ status = 204 ,
603
+ description = "User is scheduled for deletion and will be deleted" ,
604
+ body = EmptyApiResponse ,
605
+ content_type = "application/json"
606
+ )
607
+ )
608
+ ) ]
609
+ pub async fn delete (
610
+ State ( cx) : State < ServerContext > ,
611
+ Extension ( Session { user, .. } ) : Extension < Session > ,
612
+ ) -> api:: Result < ( ) > {
613
+ ops:: db:: user:: delete ( cx, user)
614
+ . await
615
+ . inspect_err ( |e| {
616
+ sentry_eyre:: capture_report ( e) ;
617
+ tracing:: error!( error = %e, "failed to delete user" ) ;
618
+ } )
619
+ . map_err ( |_| api:: internal_server_error ( ) ) ?;
620
+
621
+ Ok ( api:: no_content ( ) )
622
+ }
0 commit comments