wip: currently rewriting the project as a full stack application tangled.org/kacaii.dev/sigo
gleam

:passport_control: use session module for authentication

+159 -214
+1 -6
src/app/domain/admin/admin_update_user.gleam
··· 136 136 user_id: String, 137 137 ) -> Result(String, AdminUpdateUserError) { 138 138 use _ <- result.try( 139 - user.check_role_authorization( 140 - request: req, 141 - ctx:, 142 - cookie_name: user.uuid_cookie_name, 143 - authorized_roles: [role.Admin, role.Developer], 144 - ) 139 + user.check_authorization(req, [role.Admin, role.Developer]) 145 140 |> result.map_error(AccessControl), 146 141 ) 147 142
+5 -7
src/app/domain/brigade/register_new_brigade.gleam
··· 100 100 } 101 101 102 102 fn query_database( 103 - request request: wisp.Request, 103 + request req: wisp.Request, 104 104 ctx ctx: Context, 105 105 body body: RequestBody, 106 106 ) -> Result(String, RegisterBrigadeError) { 107 107 use _ <- result.try( 108 - user.check_role_authorization( 109 - request:, 110 - ctx:, 111 - cookie_name: user.uuid_cookie_name, 112 - authorized_roles: [role.Admin, role.Developer], 113 - ) 108 + user.check_authorization(request: req, authorized_roles: [ 109 + role.Admin, 110 + role.Developer, 111 + ]) 114 112 |> result.map_error(AccessControl), 115 113 ) 116 114
+5 -6
src/app/domain/data_analysis/analysis_occurrence_volume.gleam
··· 64 64 65 65 fn query_database(req: wisp.Request, ctx: Context) { 66 66 use _ <- result.try( 67 - user.check_role_authorization( 68 - request: req, 69 - ctx:, 70 - cookie_name: user.uuid_cookie_name, 71 - authorized_roles: [role.Admin, role.Developer, role.Analyst], 72 - ) 67 + user.check_authorization(request: req, authorized_roles: [ 68 + role.Admin, 69 + role.Developer, 70 + role.Analyst, 71 + ]) 73 72 |> result.map_error(AccessControl), 74 73 ) 75 74
+6 -6
src/app/domain/notification/get_notification_preferences.gleam
··· 1 1 import app/domain/notification/sql 2 2 import app/domain/occurrence/category 3 - import app/domain/user 4 3 import app/web 5 4 import app/web/context.{type Context} 5 + import app/web/session 6 6 import gleam/dict 7 7 import gleam/http 8 8 import gleam/json ··· 38 38 39 39 fn handle_error(err: GetNotificationPreferencesError) -> wisp.Response { 40 40 case err { 41 - AccessControl(err) -> user.handle_authentication_error(err) 41 + AccessControl(err) -> session.handle_error(err) 42 42 DatabaseError(err) -> web.handle_database_error(err) 43 43 } 44 44 } ··· 47 47 req: wisp.Request, 48 48 ctx: Context, 49 49 ) -> Result(String, GetNotificationPreferencesError) { 50 - use user_uuid <- result.try( 51 - user.extract_uuid(request: req, cookie_name: user.uuid_cookie_name) 50 + use token <- result.try( 51 + session.extract(req) 52 52 |> result.map_error(AccessControl), 53 53 ) 54 54 55 55 use returned <- result.try( 56 - sql.query_notification_preferences(ctx.db, user_uuid) 56 + sql.query_notification_preferences(ctx.db, token.user_id) 57 57 |> result.map_error(DatabaseError), 58 58 ) 59 59 ··· 77 77 /// Querying the user notification preferences can fail 78 78 type GetNotificationPreferencesError { 79 79 /// Authentication failed 80 - AccessControl(user.AuthenticationError) 80 + AccessControl(session.SessionError) 81 81 /// An error occurred while querying the DataBase 82 82 DatabaseError(pog.QueryError) 83 83 }
+6 -6
src/app/domain/notification/update_notification_preferences.gleam
··· 1 1 import app/domain/notification/sql 2 2 import app/domain/occurrence/category 3 - import app/domain/user 4 3 import app/web 5 4 import app/web/context.{type Context} 5 + import app/web/session 6 6 import gleam/dict 7 7 import gleam/dynamic/decode 8 8 import gleam/http ··· 40 40 41 41 type UpdateNotificationPreferencesError { 42 42 /// Authentication failed 43 - AccessControl(user.AuthenticationError) 43 + AccessControl(session.SessionError) 44 44 /// Failed to query Database 45 45 DataBase(pog.QueryError) 46 46 NotFound ··· 78 78 79 79 fn handle_error(err: UpdateNotificationPreferencesError) -> wisp.Response { 80 80 case err { 81 - AccessControl(err) -> user.handle_authentication_error(err) 81 + AccessControl(err) -> session.handle_error(err) 82 82 DataBase(err) -> web.handle_database_error(err) 83 83 NotFound -> 84 84 "O banco de dados não retornou resultados após atualizar as preferências" ··· 95 95 dict.Dict(category.Category, Bool), 96 96 UpdateNotificationPreferencesError, 97 97 ) { 98 - use user_uuid <- result.try( 99 - user.extract_uuid(request: req, cookie_name: user.uuid_cookie_name) 98 + use token <- result.try( 99 + session.extract(req) 100 100 |> result.map_error(AccessControl), 101 101 ) 102 102 ··· 113 113 } 114 114 115 115 use returned <- result.try( 116 - sql.update_notification_preferences(ctx.db, user_uuid, key, value) 116 + sql.update_notification_preferences(ctx.db, token.user_id, key, value) 117 117 |> result.map_error(DataBase), 118 118 ) 119 119
+4 -4
src/app/domain/occurrence/close_occurrence.gleam
··· 1 1 import app/domain/occurrence 2 2 import app/domain/occurrence/sql 3 - import app/domain/user 4 3 import app/web 5 4 import app/web/context.{type Context} 5 + import app/web/session 6 6 import app/web/socket/message as msg 7 7 import gleam/http 8 8 import gleam/json ··· 23 23 /// An error occurred whe naccessing the DataBase 24 24 DataBase(pog.QueryError) 25 25 /// Errors related to authentication / authorization 26 - AccessControl(user.AuthenticationError) 26 + AccessControl(session.SessionError) 27 27 } 28 28 29 29 /// 󰚰 Updates the `resolved_at` field of a occurrence ··· 54 54 occ_id: String, 55 55 ) -> Result(String, ResolveOccurrenceError) { 56 56 use _ <- result.try( 57 - user.extract_uuid(request: req, cookie_name: user.uuid_cookie_name) 57 + session.extract(req) 58 58 |> result.map_error(AccessControl), 59 59 ) 60 60 ··· 102 102 103 103 fn handle_error(err: ResolveOccurrenceError) -> wisp.Response { 104 104 case err { 105 - AccessControl(err) -> user.handle_authentication_error(err) 105 + AccessControl(err) -> session.handle_error(err) 106 106 DataBase(err) -> web.handle_database_error(err) 107 107 InvalidUuid(id) -> 108 108 wisp.bad_request("Ocorrência possui Uuid inválido: " <> id)
+6 -6
src/app/domain/occurrence/delete_occurrence.gleam
··· 1 1 import app/domain/occurrence/sql 2 - import app/domain/user 3 2 import app/web 4 3 import app/web/context.{type Context} 4 + import app/web/session 5 5 import gleam/http 6 6 import gleam/json 7 7 import gleam/list ··· 15 15 /// 󰿀 Occurrence has invalid Uuid 16 16 InvalidUuid(String) 17 17 ///  Authentication failed 18 - AuthenticationError(user.AuthenticationError) 18 + AccessControl(session.SessionError) 19 19 /// 󱙀 Failed to query the DataBase 20 20 DataBase(pog.QueryError) 21 21 /// 󱪘 Occurrence was not found in the system ··· 55 55 |> result.replace_error(InvalidUuid(id_str)), 56 56 ) 57 57 58 - use _user_uuid <- result.try( 58 + use _ <- result.try( 59 59 // Armazenamos o UUID do usuário caso precisemos para autorização 60 - user.extract_uuid(request: req, cookie_name: user.uuid_cookie_name) 61 - |> result.map_error(AuthenticationError), 60 + session.extract(req) 61 + |> result.map_error(AccessControl), 62 62 ) 63 63 64 64 use returned <- result.try( ··· 81 81 InvalidUuid(uuid_string) -> 82 82 // 404 Bad Request 83 83 wisp.bad_request("UUID inválido: " <> uuid_string) 84 - AuthenticationError(auth_err) -> user.handle_authentication_error(auth_err) 84 + AccessControl(err) -> session.handle_error(err) 85 85 DataBase(db_err) -> web.handle_database_error(db_err) 86 86 OccurrenceNotFound(occ_uuid) -> { 87 87 // 404 not found
+6 -5
src/app/domain/occurrence/register_new_occurrence.gleam
··· 9 9 import app/domain/user 10 10 import app/web 11 11 import app/web/context.{type Context} 12 + import app/web/session 12 13 import app/web/socket/message as msg 13 14 import gleam/dynamic/decode 14 15 import gleam/http ··· 85 86 86 87 type RegisterNewOccurrenceError { 87 88 /// Failed to authenticate the user 88 - AccessControl(user.AuthenticationError) 89 + AccessControl(session.SessionError) 89 90 /// Failed to access the database 90 91 DataBase(pog.QueryError) 91 92 /// Database returned no results ··· 96 97 97 98 fn handle_error(err: RegisterNewOccurrenceError) -> wisp.Response { 98 99 case err { 99 - AccessControl(err) -> user.handle_authentication_error(err) 100 + AccessControl(err) -> session.handle_error(err) 100 101 DataBase(err) -> web.handle_database_error(err) 101 102 FailedToAssignBrigade(id) -> { 102 103 let body = "Não foi possível designar a equipe: " <> uuid.to_string(id) ··· 145 146 ctx ctx: Context, 146 147 body body: RequestBody, 147 148 ) -> Result(String, RegisterNewOccurrenceError) { 148 - use applicant_uuid <- result.try( 149 - user.extract_uuid(request:, cookie_name: user.uuid_cookie_name) 149 + use token <- result.try( 150 + session.extract(request) 150 151 |> result.map_error(AccessControl), 151 152 ) 152 153 153 154 use returned <- result.try( 154 155 sql.insert_new_occurence( 155 156 ctx.db, 156 - applicant_uuid, 157 + token.user_id, 157 158 category_to_enum(body.occurrence_category), 158 159 subcategory_to_enum(body.occurrence_subcategory), 159 160 priority_to_enum(body.priority),
+4 -4
src/app/domain/occurrence/reopen_occurrence.gleam
··· 1 1 import app/domain/occurrence 2 2 import app/domain/occurrence/sql 3 - import app/domain/user 4 3 import app/web 5 4 import app/web/context.{type Context} 5 + import app/web/session 6 6 import app/web/socket/message as msg 7 7 import gleam/http 8 8 import gleam/json ··· 23 23 /// An error occurred whe naccessing the DataBase 24 24 DataBase(pog.QueryError) 25 25 /// Errors related to authentication / authorization 26 - AccessControl(user.AuthenticationError) 26 + AccessControl(session.SessionError) 27 27 } 28 28 29 29 /// 󰚰 Updates the `resolved_at` field of a occurrence ··· 54 54 occ_id: String, 55 55 ) -> Result(String, ResolveOccurrenceError) { 56 56 use _ <- result.try( 57 - user.extract_uuid(request: req, cookie_name: user.uuid_cookie_name) 57 + session.extract(req) 58 58 |> result.map_error(AccessControl), 59 59 ) 60 60 ··· 101 101 102 102 fn handle_error(err: ResolveOccurrenceError) -> wisp.Response { 103 103 case err { 104 - AccessControl(err) -> user.handle_authentication_error(err) 104 + AccessControl(err) -> session.handle_error(err) 105 105 DataBase(err) -> web.handle_database_error(err) 106 106 InvalidUuid(id) -> 107 107 wisp.bad_request("Ocorrência possui Uuid inválido: " <> id)
+13 -74
src/app/domain/user.gleam
··· 1 1 import app/domain/role 2 - import app/domain/user/sql 3 2 import app/web 4 - import app/web/context.{type Context} 3 + import app/web/session 5 4 import app/web/socket/message as msg 6 5 import gleam/erlang/process 7 6 import gleam/json ··· 17 16 ///  Errors related to user access control (authentication & authorization) 18 17 pub type AccessControlError { 19 18 /// 󰗹 Authentication failed 20 - Authentication(AuthenticationError) 19 + Authentication(session.SessionError) 21 20 ///  User is authentication but lacks permissions 22 21 AuthorizationError( 23 22 user_uuid: uuid.Uuid, ··· 53 52 54 53 use member <- list.each(members) 55 54 process.spawn(fn() { process.send(member, message) }) 56 - } 57 - 58 - ///  Query the database to find the user's role name 59 - pub fn get_user_role( 60 - ctx ctx: Context, 61 - user_uuid id: uuid.Uuid, 62 - ) -> Result(role.Role, AccessControlError) { 63 - use returned <- result.try( 64 - sql.query_user_role(ctx.db, id) 65 - |> result.map_error(DataBase), 66 - ) 67 - 68 - use row <- result.map( 69 - list.first(returned.rows) 70 - |> result.replace_error(RoleNotFound), 71 - ) 72 - 73 - case row.user_role { 74 - sql.Admin -> role.Admin 75 - sql.Analyst -> role.Analyst 76 - sql.Captain -> role.Captain 77 - sql.Developer -> role.Developer 78 - sql.Firefighter -> role.Firefighter 79 - sql.Sargeant -> role.Sargeant 80 - } 81 - } 82 - 83 - ///  Extracts the user UUID from the request's Cookie 84 - pub fn extract_uuid( 85 - request request: wisp.Request, 86 - cookie_name cookie_name: String, 87 - ) -> Result(uuid.Uuid, AuthenticationError) { 88 - use maybe_uuid <- result.try( 89 - wisp.get_cookie(request:, name: cookie_name, security: wisp.Signed) 90 - |> result.replace_error(MissingCookie), 91 - ) 92 - 93 - uuid.from_string(maybe_uuid) 94 - |> result.replace_error(InvalidUUID(maybe_uuid)) 95 55 } 96 56 97 57 /// 󰡦 Extracts the user UUID from the request and query the DataBase 98 58 /// to verify if the user has authorization to access determined endpoint 99 - pub fn check_role_authorization( 59 + pub fn check_authorization( 100 60 request request: wisp.Request, 101 - ctx ctx: Context, 102 - cookie_name cookie_name: String, 103 61 authorized_roles authorized_roles: List(role.Role), 104 - ) -> Result(#(uuid.Uuid, role.Role), AccessControlError) { 105 - use user_uuid <- result.try( 106 - extract_uuid(request, cookie_name) 62 + ) -> Result(role.Role, AccessControlError) { 63 + use token <- result.try( 64 + session.extract(request) 107 65 |> result.map_error(Authentication), 108 66 ) 109 67 110 - // 󰯦 Query the User's role 111 - use user_role <- result.try(get_user_role(ctx, user_uuid)) 112 - 113 68 // 󰈞 Check if that role has authorization 114 - use user_role <- result.map( 115 - list.find(authorized_roles, fn(authorized) { user_role == authorized }) 116 - |> result.replace_error(AuthorizationError( 117 - user_uuid:, 118 - user_role:, 119 - authorized_roles:, 120 - )), 121 - ) 122 - 123 - #(user_uuid, user_role) 124 - } 125 - 126 - pub fn handle_authentication_error(err: AuthenticationError) { 127 - case err { 128 - InvalidUUID(id) -> 129 - wisp.Text("ID de usuário inválido: " <> id) 130 - |> wisp.set_body(wisp.response(401), _) 131 - MissingCookie -> 132 - "Cookie de autenticação ausente" 133 - |> wisp.Text 134 - |> wisp.set_body(wisp.response(401), _) 135 - } 69 + list.find(authorized_roles, fn(authorized) { token.user_role == authorized }) 70 + |> result.replace_error(AuthorizationError( 71 + user_uuid: token.user_id, 72 + user_role: token.user_role, 73 + authorized_roles:, 74 + )) 136 75 } 137 76 138 77 pub fn handle_access_control_error(err: AccessControlError) { 139 78 case err { 140 - Authentication(err) -> handle_authentication_error(err) 79 + Authentication(err) -> session.handle_error(err) 141 80 DataBase(err) -> web.handle_database_error(err) 142 81 RoleNotFound -> 143 82 "Não foi possível confirmar o Cargo do usuário autenticado"
+15 -9
src/app/domain/user/delete_user.gleam
··· 3 3 import app/domain/user/sql 4 4 import app/web 5 5 import app/web/context.{type Context} 6 + import app/web/session 6 7 import gleam/http 7 8 import gleam/json 8 9 import gleam/list ··· 44 45 UserNotFound(uuid.Uuid) 45 46 /// 󱅞 An user should not be able to delete theirself 46 47 CantDeleteSelf 48 + Authentication(session.SessionError) 47 49 } 48 50 49 51 fn handle_error(err: DeleteUserError) -> wisp.Response { ··· 55 57 AccessControl(err) -> user.handle_access_control_error(err) 56 58 DataBase(err) -> web.handle_database_error(err) 57 59 CantDeleteSelf -> wisp.bad_request("Um usuário não deve remover a si mesmo") 60 + Authentication(err) -> session.handle_error(err) 58 61 } 59 62 } 60 63 ··· 63 66 ctx: Context, 64 67 target_id: String, 65 68 ) -> Result(String, DeleteUserError) { 69 + use token <- result.try( 70 + session.extract(req) 71 + |> result.map_error(Authentication), 72 + ) 73 + 66 74 use target_user_uuid <- result.try( 67 75 uuid.from_string(target_id) 68 76 |> result.replace_error(InvalidUserUuid(target_id)), 69 77 ) 70 78 71 - use #(user_uuid, _) <- result.try( 72 - user.check_role_authorization( 73 - request: req, 74 - ctx:, 75 - cookie_name: user.uuid_cookie_name, 76 - authorized_roles: [role.Admin, role.Developer], 77 - ) 79 + use _ <- result.try( 80 + user.check_authorization(request: req, authorized_roles: [ 81 + role.Admin, 82 + role.Developer, 83 + ]) 78 84 |> result.map_error(AccessControl), 79 85 ) 80 86 81 - case uuid.to_string(user_uuid) == target_id { 87 + case uuid.to_string(token.user_id) == target_id { 82 88 True -> Error(CantDeleteSelf) 83 89 False -> { 84 90 use returned <- result.try( ··· 88 94 89 95 use row <- result.map( 90 96 list.first(returned.rows) 91 - |> result.replace_error(UserNotFound(user_uuid)), 97 + |> result.replace_error(UserNotFound(token.user_id)), 92 98 ) 93 99 94 100 [
+4 -6
src/app/domain/user/get_all_user_profiles.gleam
··· 62 62 ctx: Context, 63 63 ) -> Result(String, GetAllUsersError) { 64 64 use _ <- result.try( 65 - user.check_role_authorization( 66 - request: req, 67 - ctx:, 68 - cookie_name: user.uuid_cookie_name, 69 - authorized_roles: [role.Admin, role.Developer], 70 - ) 65 + user.check_authorization(request: req, authorized_roles: [ 66 + role.Admin, 67 + role.Developer, 68 + ]) 71 69 |> result.map_error(AccessControl), 72 70 ) 73 71
+8 -8
src/app/domain/user/get_user_profile.gleam
··· 1 1 import app/domain/role 2 - import app/domain/user 3 2 import app/domain/user/sql 4 3 import app/web 5 4 import app/web/context.{type Context} 5 + import app/web/session 6 6 import gleam/http 7 7 import gleam/json 8 8 import gleam/list ··· 48 48 ///  User not found in the DataBase 49 49 UserNotFound(uuid.Uuid) 50 50 ///  Authentication failed 51 - AccessControl(user.AuthenticationError) 51 + Session(session.SessionError) 52 52 } 53 53 54 54 fn handle_error(err: GetUserProfileError) { 55 55 case err { 56 - AccessControl(err) -> user.handle_authentication_error(err) 56 + Session(err) -> session.handle_error(err) 57 57 DataBase(err) -> web.handle_database_error(err) 58 58 UserNotFound(id) -> { 59 59 // HTTP 404 Not Found ··· 67 67 } 68 68 69 69 pub fn query_user_data(ctx: Context, request: wisp.Request) { 70 - use user_id <- result.try( 71 - user.extract_uuid(request:, cookie_name: user.uuid_cookie_name) 72 - |> result.map_error(AccessControl), 70 + use token <- result.try( 71 + session.extract(request) 72 + |> result.map_error(Session), 73 73 ) 74 74 75 75 use returned <- result.try( 76 - sql.query_user_profile(ctx.db, user_id) 76 + sql.query_user_profile(ctx.db, token.user_id) 77 77 |> result.map_error(DataBase), 78 78 ) 79 79 80 80 use row <- result.try( 81 81 list.first(returned.rows) 82 - |> result.replace_error(UserNotFound(user_id)), 82 + |> result.replace_error(UserNotFound(token.user_id)), 83 83 ) 84 84 85 85 let user_role =
+18 -22
src/app/domain/user/login.gleam
··· 3 3 ////  Uses signed cookies to prevent tampering and logs all login attempts. 4 4 5 5 import app/domain/role 6 - import app/domain/user 7 6 import app/domain/user/sql 8 7 import app/web 9 8 import app/web/context.{type Context} 9 + import app/web/session 10 10 import argus 11 11 import gleam/dynamic/decode 12 12 import gleam/float ··· 18 18 import gleam/time/duration 19 19 import pog 20 20 import wisp 21 - import youid/uuid 22 21 23 22 type RequestBody { 24 23 RequestBody(registration: String, password: String) ··· 35 34 HashError 36 35 } 37 36 38 - type LoginToken { 39 - LoginToken(body: String, user_id: uuid.Uuid) 40 - } 41 - 42 37 ///  Handles user login authentication and session management 43 38 /// On success, sets a cookie on the client containing the User UUID, 44 39 /// It will then be used on later requests for authetication. 45 40 /// 46 41 /// The actual Cookie is **encrypted**. 47 42 /// 48 - /// ## Example 49 - /// 50 - /// ```sh 51 - /// set-cookie: USER_ID=0199b58a-acb0-70a8-9de7-0b65a03b8743 52 - /// ``` 43 + /// TODO: 53 44 pub fn handle_request(request req: wisp.Request, ctx ctx: Context) { 54 45 use <- wisp.require_method(req, http.Post) 55 46 use body <- wisp.require_json(req) 56 47 57 48 case decode.run(body, body_decoder()) { 58 49 Error(err) -> web.handle_decode_error(err) 59 - Ok(data) -> handle_login(req, ctx, data, user.uuid_cookie_name) 50 + Ok(data) -> handle_login(req, ctx, data, session.cookie_name) 60 51 } 61 52 } 62 53 ··· 72 63 } 73 64 } 74 65 75 - fn set_token(resp: LoginToken, req: wisp.Request, name: String) -> wisp.Response { 76 - let response = wisp.json_response(resp.body, 200) 77 - let value = uuid.to_string(resp.user_id) 66 + fn set_token( 67 + session: session.Session, 68 + req: wisp.Request, 69 + name: String, 70 + ) -> wisp.Response { 71 + let response = 72 + [#("access_level", session.user_role |> role.to_string |> json.string)] 73 + |> json.object 74 + |> json.to_string 75 + |> wisp.json_response(200) 76 + 77 + let value = session.to_json(session) |> json.to_string 78 78 let security = wisp.Signed 79 79 80 80 let max_age = ··· 88 88 fn body_decoder() -> decode.Decoder(RequestBody) { 89 89 use registration <- decode.field("matricula", decode.string) 90 90 use password <- decode.field("senha", decode.string) 91 + 91 92 RequestBody(registration:, password:) 92 93 |> decode.success 93 94 } ··· 118 119 fn query_database( 119 120 data: RequestBody, 120 121 ctx: Context, 121 - ) -> Result(LoginToken, LoginError) { 122 + ) -> Result(session.Session, LoginError) { 122 123 use returned <- result.try( 123 124 sql.query_login_token(ctx.db, data.registration) 124 125 |> result.map_error(DataBase), ··· 140 141 sql.Sargeant -> role.Sargeant 141 142 } 142 143 143 - json.object([ 144 - #("id", json.string(uuid.to_string(row.id))), 145 - #("role", json.string(role.to_string_pt_br(user_role))), 146 - ]) 147 - |> json.to_string 148 - |> LoginToken(body: _, user_id: row.id) 144 + session.Session(user_id: row.id, user_role:) 149 145 } 150 146 151 147 fn verify_correct_password(
+4 -6
src/app/domain/user/signup.gleam
··· 55 55 ctx ctx: Context, 56 56 ) -> Result(String, SignupError) { 57 57 use _ <- result.try( 58 - user.check_role_authorization( 59 - request:, 60 - ctx:, 61 - cookie_name: user.uuid_cookie_name, 62 - authorized_roles: [role.Admin, role.Developer], 63 - ) 58 + user.check_authorization(request:, authorized_roles: [ 59 + role.Admin, 60 + role.Developer, 61 + ]) 64 62 |> result.map_error(AccessControl), 65 63 ) 66 64
+11 -8
src/app/domain/user/update_user_password.gleam
··· 1 - import app/domain/user 2 1 import app/domain/user/sql 3 2 import app/web 4 3 import app/web/context.{type Context} 4 + import app/web/session 5 5 import argus 6 6 import gleam/bool 7 7 import gleam/dynamic/decode ··· 50 50 51 51 fn handle_error(err: UpdatePasswordError) -> wisp.Response { 52 52 case err { 53 - AccessControl(err) -> user.handle_authentication_error(err) 53 + Session(err) -> session.handle_error(err) 54 54 UserNotFound(id) -> { 55 55 let body = "Usuário não encontrado: " <> uuid.to_string(id) 56 56 ··· 79 79 ctx ctx: Context, 80 80 form_data form_data: RequestBody, 81 81 ) -> Result(pog.Returned(Nil), UpdatePasswordError) { 82 - use user_uuid <- result.try( 83 - user.extract_uuid(request: req, cookie_name: user.uuid_cookie_name) 84 - |> result.map_error(AccessControl), 82 + use token <- result.try( 83 + session.extract(req) 84 + |> result.map_error(Session), 85 85 ) 86 86 87 87 // Fetch the password hash from the DataBase 88 - use current_password_hash <- result.try(query_user_password(ctx:, user_uuid:)) 88 + use current_password_hash <- result.try(query_user_password( 89 + ctx:, 90 + user_uuid: token.user_id, 91 + )) 89 92 90 93 // Compare the current password with the hash 91 94 use typed_correct_password <- result.try( ··· 117 120 |> result.replace_error(HashError), 118 121 ) 119 122 120 - sql.update_user_password(ctx.db, user_uuid, hashed_password.encoded_hash) 123 + sql.update_user_password(ctx.db, token.user_id, hashed_password.encoded_hash) 121 124 |> result.map_error(DataBase) 122 125 } 123 126 ··· 161 164 ///  Updating an user's password can fail 162 165 type UpdatePasswordError { 163 166 ///  Authentication failed 164 - AccessControl(user.AuthenticationError) 167 + Session(session.SessionError) 165 168 ///  User was not found in the database 166 169 UserNotFound(uuid.Uuid) 167 170 /// 󱙀 Failed to access the DataBase
+7 -7
src/app/domain/user/update_user_profile.gleam
··· 1 - import app/domain/user 2 1 import app/domain/user/sql 3 2 import app/web 4 3 import app/web/context.{type Context} 4 + import app/web/session 5 5 import gleam/dynamic/decode 6 6 import gleam/http 7 7 import gleam/json ··· 49 49 50 50 fn handle_error(err: UpdateProfileError) -> wisp.Response { 51 51 case err { 52 - Authentication(err) -> user.handle_authentication_error(err) 52 + Authentication(err) -> session.handle_error(err) 53 53 Database(err) -> handle_database_error(err) 54 54 NotFound(id) -> 55 55 wisp.Text("Usuário não encontrado" <> uuid.to_string(id)) ··· 80 80 ctx: Context, 81 81 body: RequestBody, 82 82 ) -> Result(String, UpdateProfileError) { 83 - use maybe_id <- result.try( 84 - user.extract_uuid(req, user.uuid_cookie_name) 83 + use token <- result.try( 84 + session.extract(req) 85 85 |> result.map_error(Authentication), 86 86 ) 87 87 88 88 use returned <- result.try( 89 89 sql.update_user_profile( 90 90 ctx.db, 91 - maybe_id, 91 + token.user_id, 92 92 body.full_name, 93 93 body.email, 94 94 body.phone, ··· 98 98 99 99 use row <- result.map( 100 100 list.first(returned.rows) 101 - |> result.replace_error(NotFound(maybe_id)), 101 + |> result.replace_error(NotFound(token.user_id)), 102 102 ) 103 103 104 104 [ ··· 112 112 113 113 type UpdateProfileError { 114 114 /// Authentication failed 115 - Authentication(user.AuthenticationError) 115 + Authentication(session.SessionError) 116 116 /// An error occurred when accessing the DataBase 117 117 Database(pog.QueryError) 118 118 /// User was not found in the DataBase
+4 -6
src/app/domain/user/update_user_status.gleam
··· 67 67 is_active is_active: Bool, 68 68 ) -> Result(String, UpdateUserStatusError) { 69 69 use _ <- result.try( 70 - user.check_role_authorization( 71 - request: req, 72 - ctx: ctx, 73 - cookie_name: user.uuid_cookie_name, 74 - authorized_roles: [role.Admin, role.Developer], 75 - ) 70 + user.check_authorization(request: req, authorized_roles: [ 71 + role.Admin, 72 + role.Developer, 73 + ]) 76 74 |> result.map_error(AccessControl), 77 75 ) 78 76
+27 -9
src/app/web/session.gleam
··· 5 5 import wisp 6 6 import youid/uuid 7 7 8 - pub const session_cookie_name = "SESSION" 8 + pub const cookie_name = "SESSION" 9 9 10 10 pub type SessionError { 11 + ///  Request is missing the authetication Cookie 11 12 MissingCookie 12 - Decode(json.DecodeError) 13 + ///  Failed to decode into a `Session` type 14 + InvalidToken 13 15 } 14 16 15 17 pub type Session { ··· 18 20 19 21 pub fn extract(req: wisp.Request) { 20 22 use raw <- result.try( 21 - wisp.get_cookie( 22 - request: req, 23 - name: session_cookie_name, 24 - security: wisp.Signed, 25 - ) 23 + wisp.get_cookie(request: req, name: cookie_name, security: wisp.Signed) 26 24 |> result.replace_error(MissingCookie), 27 25 ) 28 26 29 27 json.parse(raw, decoder()) 30 - |> result.map_error(Decode) 28 + |> result.replace_error(InvalidToken) 29 + } 30 + 31 + pub fn handle_error(err: SessionError) -> wisp.Response { 32 + case err { 33 + InvalidToken -> 34 + "Token de Autorização inválido" 35 + |> wisp.Text 36 + |> wisp.set_body(wisp.response(401), _) 37 + MissingCookie -> 38 + "Cookie de autenticação ausente" 39 + |> wisp.Text 40 + |> wisp.set_body(wisp.response(401), _) 41 + } 42 + } 43 + 44 + pub fn to_json(session: Session) -> json.Json { 45 + json.object([ 46 + #("user_id", session.user_id |> uuid.to_string |> json.string), 47 + #("user_role", session.user_role |> role.to_json), 48 + ]) 31 49 } 32 50 33 51 pub fn decoder() -> decode.Decoder(Session) { ··· 36 54 decode.success(Session(user_id:, user_role:)) 37 55 } 38 56 39 - fn uuid_decoder() { 57 + fn uuid_decoder() -> decode.Decoder(uuid.Uuid) { 40 58 use field <- decode.then(decode.string) 41 59 case uuid.from_string(field) { 42 60 Error(_) -> decode.failure(uuid.v7(), "uuid")
+5 -9
test/user_test.gleam
··· 1 1 import app/domain/role 2 - import app/domain/user 3 2 import app/http_router 3 + import app/web/session 4 4 import app_dev/sql as dev_sql 5 5 import app_test 6 6 import dummy ··· 35 35 36 36 let assert Ok(_) = 37 37 json.parse(body, { 38 - use maybe_uuid <- decode.field("id", decode.string) 39 - let assert Ok(_) = uuid.from_string(maybe_uuid) as "Invalid UUID" 40 - 41 - use maybe_role <- decode.field("role", decode.string) 42 - let assert Ok(_) = role.from_string_pt_br(maybe_role) 43 - as "Invalid user Role" 38 + use maybe_role <- decode.field("access_level", decode.string) 39 + let assert Ok(_) = role.from_string(maybe_role) as "Invalid user Role" 44 40 45 41 decode.success(Nil) 46 42 }) ··· 49 45 let cookies = response.get_cookies(resp) 50 46 assert cookies != [] as "Server should set a session Cookie on login" 51 47 52 - let assert Ok(_) = list.key_find(cookies, user.uuid_cookie_name) 53 - as "No Cookie named USER_ID was found in the server response" 48 + let assert Ok(_) = list.key_find(cookies, session.cookie_name) 49 + as "Target cookie found in the server response" 54 50 } 55 51 56 52 pub fn signup_test() {