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

Configure Feed

Select the types of activity you want to include in your feed.

:sparkles: finish new occurrence route

Kacaii 8f0ba760 d19e0d85

+616 -131
+8 -7
README.md
··· 8 8 9 9 ## Routes 10 10 11 - | Route | Description | Method | 12 - | ---------------------------------------------- | ---------------------------------------- | ----------- | 13 - | /api/user/signup | Register a new user account | POST (Form) | 14 - | /api/user/login | Login with your user account | POST (Form) | 15 - | /api/user/get_occurrences/{user_id} | Get all occurrences applied by this user | GET | 16 - | /api/user/get_fellow_brigade_members/{user_id} | List fellow brigade members of this user | GET | 17 - | /api/brigade/get_members/{brigade_id} | List brigade members | GET | 11 + | Route | Description | Method | 12 + | --------------------------- | ---------------------------------------- | ----------- | 13 + | /api/user/signup | Register a new user account | POST (Form) | 14 + | /api/user/login | Login with your user account | POST (Form) | 15 + | /api/occurrence/new | Register new occurrence | POST (Form) | 16 + | /api/user/{id}/occurrences | Get all occurrences applied by this user | GET | 17 + | /api/user/{id}/crew_members | List fellow brigade members of this user | GET | 18 + | /api/brigade/{id}/members | List brigade members | GET | 18 19 19 20 ## Entity RelationShip Diagram 20 21
+1
justfile
··· 38 38 [group(' postgres')] 39 39 [group(' ship')] 40 40 @rebuild_empty: 41 + @psql senac_brigade -f priv/sql/create/triggers.sql 41 42 @psql senac_brigade -f priv/sql/create/tables.sql 42 43 @psql senac_brigade -f priv/sql/create/functions.sql 43 44
+19 -7
priv/sql/create/tables.sql
··· 7 7 DROP INDEX IF EXISTS public.idx_user_registration; 8 8 DROP INDEX IF EXISTS public.idx_user_id; 9 9 DROP INDEX IF EXISTS public.idx_occurrence_id; 10 + DROP INDEX IF EXISTS public.idx_occurrence_brigade_member_user_id; 11 + DROP INDEX IF EXISTS public.idx_occurrence_brigade_member_occurrence_id; 10 12 11 13 -- pgt-ignore-start lint/safety/banDropTable: We are resetting the Database 12 14 DROP TABLE IF EXISTS public.occurrence; 13 15 DROP TABLE IF EXISTS public.occurrence_category; 16 + DROP TABLE IF EXISTS public.occurrence_brigade_member; 14 17 DROP TABLE IF EXISTS public.brigade_membership; 15 18 DROP TABLE IF EXISTS public.brigade; 16 19 DROP TABLE IF EXISTS public.user_account; ··· 41 44 42 45 CREATE INDEX IF NOT EXISTS idx_user_registration 43 46 ON public.user_account (registration); 44 - 45 - CREATE INDEX IF NOT EXISTS idx_user_id 46 - ON public.user_account (id); 47 47 48 48 CREATE TABLE IF NOT EXISTS public.brigade ( 49 49 id UUID PRIMARY KEY DEFAULT UUIDV7(), ··· 85 85 ON UPDATE CASCADE ON DELETE SET NULL DEFAULT NULL, 86 86 description TEXT, 87 87 location POINT NOT NULL, 88 - reference_point TEXT NOT NULL, 89 - loss_percentage NUMERIC(2), 88 + reference_point TEXT, 89 + vehicle_code TEXT NOT NULL, 90 + participants_id UUID [], 90 91 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 91 92 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 92 93 resolved_at TIMESTAMP DEFAULT NULL ··· 95 96 CREATE INDEX IF NOT EXISTS idx_occurrence_applicant_id 96 97 ON public.occurrence (applicant_id); 97 98 98 - CREATE INDEX IF NOT EXISTS idx_occurrence_id 99 - ON public.occurrence (id); 99 + CREATE TABLE IF NOT EXISTS public.occurrence_brigade_member ( 100 + id UUID PRIMARY KEY DEFAULT UUIDV7(), 101 + user_id UUID REFERENCES public.user_account (id) 102 + ON UPDATE CASCADE, 103 + occurrence_id UUID REFERENCES public.user_account (id) 104 + ON UPDATE CASCADE 105 + ); 106 + 107 + CREATE INDEX IF NOT EXISTS idx_occurrence_brigade_member_user_id 108 + ON public.occurrence_brigade_member (user_id); 109 + 110 + CREATE INDEX IF NOT EXISTS idx_occurrence_brigade_member_occurrence_id 111 + ON public.occurrence_brigade_member (occurrence_id); 100 112 101 113 COMMIT;
+21
priv/sql/create/triggers.sql
··· 1 + DROP FUNCTION IF EXISTS public.dump_occurrence_participants; 2 + DROP TRIGGER IF EXISTS tgr_insert_member_participation ON occurrence; 3 + 4 + --  5 + CREATE OR REPLACE FUNCTION public.dump_occurrence_participants() 6 + RETURNS TRIGGER AS $$ 7 + BEGIN 8 + INSERT INTO public.occurrence_brigade_member (brigade_id, user_id) 9 + SELECT NEW.id, unnest(participants_id) 10 + FROM public.occurrence AS oc 11 + WHERE id = NEW.id 12 + AND participants_id IS NOT NULL; 13 + 14 + RETURN NEW; 15 + END; 16 + $$ LANGUAGE plpgsql; 17 + 18 + CREATE OR REPLACE TRIGGER tgr_insert_member_participation 19 + AFTER INSERT ON public.occurrence 20 + FOR EACH ROW 21 + EXECUTE FUNCTION public.dump_occurrence_participants();
+20 -1
src/app.gleam
··· 1 + //// A web application built with the Wisp framework. 2 + //// 3 + //// This module is the main entry point for the application. It is responsible for: 4 + //// - Configuring the application's dependencies (database, HTTP server) 5 + //// - Reading necessary environment variables 6 + //// - Starting the supervision tree that manages the application's processes 7 + //// 8 + //// ## Environment Variables 9 + //// - `DATABASE_URL`: The connection URI for the PostgreSQL database 10 + //// - `COOKIE_TOKEN`: The secret key used for signing and encrypting cookies 11 + //// 12 + //// ## Architecture 13 + //// The application uses a supervisor to manage two main processes: 14 + //// 1. A PostgreSQL database connection pool using Pog 15 + //// 2. An HTTP server using Mist (with Wisp handling the web layer) 16 + //// 17 + //// The supervision strategy is `OneForOne`, meaning if either process fails, 18 + //// only that specific process will be restarted, leaving the other unaffected. 19 + 1 20 import app/router 2 21 import app/web.{Context} 3 22 import envoy ··· 63 82 |> mist.port(8000) 64 83 } 65 84 66 - supervisor.new(supervisor.RestForOne) 85 + supervisor.new(supervisor.OneForOne) 67 86 |> supervisor.add(pog_pool_child) 68 87 |> supervisor.add(mist.supervised(mist_pool_child)) 69 88 |> supervisor.start
+32 -12
src/app/router.gleam
··· 1 + //// Application router that maps HTTP requests to handler functions. 2 + //// 3 + //// This module defines the URL routing table for the application's API endpoints. 4 + //// It uses path-based routing to delegate requests to specific handler modules 5 + //// for processing. 6 + //// 7 + //// All requests are processed through the web middleware pipeline before routing. 8 + //// Unmatched routes return a 404 Not Found response. 9 + 1 10 import app/routes/get_brigade_members 2 - import app/routes/get_fellow_brigade_members 11 + import app/routes/get_crew_members 3 12 import app/routes/get_ocurrences_by_applicant 4 13 import app/routes/login 14 + import app/routes/register_new_occurrence 5 15 import app/routes/signup 6 16 import app/web.{type Context} 17 + import gleam/http 7 18 import wisp 8 19 9 20 /// Handle the incoming HTTP Requests 10 21 pub fn handle_request(request: wisp.Request, ctx: Context) -> wisp.Response { 11 22 use request <- web.middleware(request: request, context: ctx) 12 23 13 - case wisp.path_segments(request) { 14 - [] -> wisp.ok() 15 - ["api", "user", "signup"] -> signup.handle_form_submission(request:, ctx:) 16 - ["api", "user", "login"] -> login.handle_form_submission(request:, ctx:) 17 - ["api", "user", "get_occurences", user_id] -> 18 - get_ocurrences_by_applicant.handle_request(request:, ctx:, user_id:) 24 + case request.method, wisp.path_segments(request) { 25 + //  Authorization routes 26 + http.Post, ["api", "user", "signup"] -> signup.handle_form(request:, ctx:) 27 + http.Post, ["api", "user", "login"] -> login.handle_form(request:, ctx:) 19 28 20 - ["api", "brigade", "get_members", brigade_id] -> 21 - get_brigade_members.handle_request(request:, ctx:, brigade_id:) 22 - ["api", "user", "get_fellow_members", user_id] -> 23 - get_fellow_brigade_members.handle_request(request:, ctx:, user_id:) 29 + //  User data routes 30 + http.Get, ["api", "user", id, "occurrences"] -> 31 + get_ocurrences_by_applicant.handle_request(request:, ctx:, id:) 32 + http.Get, ["api", "user", id, "crew_members"] -> 33 + get_crew_members.handle_request(request:, ctx:, id:) 24 34 25 - _ -> wisp.not_found() 35 + // 󰞏 Occurrence routes 36 + http.Post, ["api", "occurence", "new"] -> 37 + register_new_occurrence.handle_form(request:, ctx:) 38 + 39 + // 󰢫 Brigade routes 40 + http.Get, ["api", "brigade", id, "members"] -> 41 + get_brigade_members.handle_request(request:, ctx:, id:) 42 + 43 + // Fallback routes 44 + _, [] -> wisp.ok() 45 + _, _ -> wisp.not_found() 26 46 } 27 47 }
+14 -2
src/app/routes/get_brigade_members.gleam
··· 1 + //// Handler for retrieving members of a specific fire brigade. 2 + //// 3 + //// It returns a list of members belonging to the specified brigade, including 4 + //// their id, full name, role, and description. 5 + 1 6 import app/sql 2 7 import app/web.{type Context} 3 8 import gleam/http ··· 8 13 import wisp 9 14 import youid/uuid 10 15 16 + /// Retrieves all members of a specific fire brigade from the database 17 + /// and returns them as formatted JSON data. 11 18 pub fn handle_request( 12 19 request req: wisp.Request, 13 20 ctx ctx: Context, 14 - brigade_id brigade_id: String, 21 + id brigade_id: String, 15 22 ) -> wisp.Response { 16 23 use <- wisp.require_method(req, http.Get) 17 24 ··· 49 56 fn get_brigade_members_row_to_json( 50 57 get_brigade_members_row: sql.GetBrigadeMembersRow, 51 58 ) -> json.Json { 52 - let sql.GetBrigadeMembersRow(full_name:, description:, role_name:) = 59 + let sql.GetBrigadeMembersRow(id:, full_name:, description:, role_name:) = 53 60 get_brigade_members_row 54 61 json.object([ 62 + #("id", json.string(uuid.to_string(id))), 55 63 #("full_name", json.string(full_name)), 56 64 #("role_name", json.string(option.unwrap(role_name, ""))), 57 65 #("description", json.string(option.unwrap(description, ""))), 58 66 ]) 59 67 } 60 68 69 + /// Represents possible errors that can occur when retrieving brigade members 70 + /// from the database 61 71 type GetBrigadeMembersError { 72 + /// The provided brigade ID is not a valid UUID format 62 73 InvalidUUID 74 + /// An error occurred while accessing the database to retrieve brigade members 63 75 DataBaseError 64 76 }
+12 -7
src/app/routes/get_fellow_brigade_members.gleam src/app/routes/get_crew_members.gleam
··· 1 + //// Handler for retrieving members from the same brigade as a given user. 2 + 1 3 import app/sql 2 4 import app/web.{type Context} 3 5 import gleam/http ··· 8 10 import wisp 9 11 import youid/uuid 10 12 13 + /// Retrieves all crew members or brigade members associated with a specific user 14 + /// from the database and returns them as formatted JSON data. 11 15 pub fn handle_request( 12 16 request req: wisp.Request, 13 17 ctx ctx: Context, 14 - user_id user_id: String, 18 + id user_id: String, 15 19 ) -> wisp.Response { 16 20 use <- wisp.require_method(req, http.Get) 17 21 ··· 21 25 |> result.replace_error(InvalidUUID), 22 26 ) 23 27 use returned <- result.try( 24 - sql.get_fellow_brigade_members(ctx.conn, user_uuid) 28 + sql.get_crew_members(ctx.conn, user_uuid) 25 29 |> result.replace_error(DataBaseError), 26 30 ) 27 31 let fellow_members_list = { 28 32 use fellow_brigade_member <- list.map(returned.rows) 29 - get_fellow_brigade_members_row_to_json(fellow_brigade_member) 33 + get_crew_members_row_to_json(fellow_brigade_member) 30 34 } 31 35 32 36 Ok(json.preprocessed_array(fellow_members_list)) ··· 53 57 InvalidUUID 54 58 } 55 59 56 - fn get_fellow_brigade_members_row_to_json( 57 - get_fellow_brigade_members_row: sql.GetFellowBrigadeMembersRow, 60 + fn get_crew_members_row_to_json( 61 + get_crew_members_row: sql.GetCrewMembersRow, 58 62 ) -> json.Json { 59 - let sql.GetFellowBrigadeMembersRow(full_name:, role_name:, description:) = 60 - get_fellow_brigade_members_row 63 + let sql.GetCrewMembersRow(id:, full_name:, role_name:, description:) = 64 + get_crew_members_row 61 65 json.object([ 66 + #("id", json.string(uuid.to_string(id))), 62 67 #("full_name", json.string(full_name)), 63 68 #("role_name", json.string(option.unwrap(role_name, ""))), 64 69 #("description", json.string(option.unwrap(description, ""))),
+15 -7
src/app/routes/get_ocurrences_by_applicant.gleam
··· 1 + //// Handler for retrieving occurrences reported by a specific applicant. 2 + //// 3 + //// It returns a list of occurrences (incidents/reports) that were submitted 4 + //// by the specified user, including detailed information about each occurrence. 5 + 1 6 import app/sql 2 7 import app/web 3 8 import gleam/float ··· 11 16 import wisp 12 17 import youid/uuid 13 18 19 + /// Fetches all occurrences/applications associated with a specific user 20 + /// from the database and returns them as JSON. 14 21 pub fn handle_request( 15 22 request request: wisp.Request, 16 23 ctx ctx: web.Context, 17 - user_id user_id: String, 24 + id user_id: String, 18 25 ) -> wisp.Response { 19 26 use <- wisp.require_method(request, http.Get) 20 27 ··· 59 66 } 60 67 } 61 68 69 + /// Represents possible errors that can occur during the search 70 + /// including invalid UUID formats for applicant 62 71 type GetOccurrencesByApplicantError { 72 + /// The provided applicant ID is not a valid UUID format 63 73 InvalidUUID 74 + /// An Error occurred when querying the database 64 75 DatabaseError(pog.QueryError) 65 76 } 66 77 ··· 68 79 get_occurences_by_applicant_row: sql.GetOccurencesByApplicantRow, 69 80 ) -> json.Json { 70 81 let sql.GetOccurencesByApplicantRow( 82 + id:, 71 83 description:, 72 84 category:, 73 85 subcategory:, ··· 75 87 resolved_at:, 76 88 location:, 77 89 reference_point:, 78 - loss_percentage:, 79 90 ) = get_occurences_by_applicant_row 80 91 json.object([ 92 + #("id", json.string(uuid.to_string(id))), 81 93 #("description", case description { 82 94 option.None -> json.null() 83 95 option.Some(value) -> json.string(value) ··· 107 119 } 108 120 }), 109 121 #("location", json.array(location, json.float)), 110 - #("reference_point", json.string(reference_point)), 111 - #("loss_percentage", case loss_percentage { 112 - option.None -> json.null() 113 - option.Some(value) -> json.float(value) 114 - }), 122 + #("reference_point", json.string(option.unwrap(reference_point, ""))), 115 123 ]) 116 124 }
+8 -4
src/app/routes/login.gleam
··· 1 + //// Handler for user authentication and login. 2 + //// 3 + ////  Uses signed cookies to prevent tampering and logs all login attempts. 4 + 1 5 import app/sql 2 6 import app/web.{type Context} 3 7 import argus ··· 29 33 }) 30 34 } 31 35 32 - ///  Verifies if a user is registred 33 - pub fn handle_form_submission(request request: wisp.Request, ctx ctx: Context) { 34 - use form_data <- wisp.require_form(request) 36 + ///  Handles user login authentication and session management 37 + pub fn handle_form(request cookie_user_uuid: wisp.Request, ctx ctx: Context) { 38 + use form_data <- wisp.require_form(cookie_user_uuid) 35 39 let form_result = 36 40 login_form() 37 41 |> form.add_values(form_data.values) ··· 49 53 //  Store UUID cookie 50 54 wisp.set_cookie( 51 55 response: wisp.ok(), 52 - request: request, 56 + request: cookie_user_uuid, 53 57 name: cookie_name, 54 58 value: uuid.to_string(user_uuid), 55 59 security: wisp.Signed,
+247
src/app/routes/register_new_occurrence.gleam
··· 1 + //// Processes occurrence registration form data, validates inputs, and creates 2 + //// a new occurrence record in the database. 3 + 4 + import app/sql 5 + import app/web.{type Context} 6 + import formal/form 7 + import gleam/list 8 + import gleam/result 9 + import gleam/string 10 + import pog 11 + import wisp 12 + import youid/uuid 13 + 14 + const cookie_user_id = "USER_ID" 15 + 16 + /// Raw form data submitted for creating an occurrence, with all IDs as strings 17 + pub opaque type OccurrenceFormData { 18 + OccurrenceFormData( 19 + category_id: String, 20 + subcategory_id: String, 21 + description: String, 22 + location: List(Float), 23 + reference_point: String, 24 + vehicle_code: String, 25 + participants_id: List(String), 26 + ) 27 + } 28 + 29 + /// Validated occurrence data with all IDs converted to UUIDs, 30 + /// ready for database insertion 31 + pub opaque type Occurrence { 32 + Occurrence( 33 + applicant_id: uuid.Uuid, 34 + category_id: uuid.Uuid, 35 + subcategory_id: uuid.Uuid, 36 + description: String, 37 + location: List(Float), 38 + reference_point: String, 39 + vehicle_code: String, 40 + participants_id: List(uuid.Uuid), 41 + ) 42 + } 43 + 44 + /// Validates and constructs an Occurrence from form data by converting string 45 + /// IDs to UUIDs and extracting the applicant ID from request cookies. 46 + fn new_occurrence( 47 + data data: OccurrenceFormData, 48 + request request: wisp.Request, 49 + ) -> Result(Occurrence, RegisterNewOccurrenceError) { 50 + use category_id <- result.try( 51 + uuid.from_string(data.category_id) 52 + |> result.replace_error(InvalidCategoryUUID(data.category_id)), 53 + ) 54 + use subcategory_id <- result.try( 55 + uuid.from_string(data.subcategory_id) 56 + |> result.replace_error(InvalidSubCategoryUUID(data.subcategory_id)), 57 + ) 58 + 59 + use participants_id <- result.try({ 60 + use id_string <- list.try_map(data.participants_id) 61 + uuid.from_string(id_string) 62 + |> result.replace_error(InvalidApplicantUUID(id_string)) 63 + }) 64 + 65 + use applicant_id <- result.try(get_user_id(request)) 66 + 67 + Ok(Occurrence( 68 + applicant_id:, 69 + category_id:, 70 + subcategory_id:, 71 + description: data.description, 72 + location: data.location, 73 + reference_point: data.reference_point, 74 + vehicle_code: data.vehicle_code, 75 + participants_id:, 76 + )) 77 + } 78 + 79 + /// Extracts and validates the user ID from a signed cookie in the request, 80 + /// returning it as a UUID. 81 + fn get_user_id( 82 + request: wisp.Request, 83 + ) -> Result(uuid.Uuid, RegisterNewOccurrenceError) { 84 + use user_id_string <- result.try( 85 + wisp.get_cookie(request:, name: cookie_user_id, security: wisp.Signed) 86 + |> result.replace_error(MissingCookie), 87 + ) 88 + 89 + use user_uuid <- result.try( 90 + uuid.from_string(user_id_string) 91 + |> result.replace_error(InvalidApplicantUUID(user_id_string)), 92 + ) 93 + 94 + Ok(user_uuid) 95 + } 96 + 97 + /// 󱐀 A form that decodes the `Occurrence` type 98 + fn occurence_form() -> form.Form(OccurrenceFormData) { 99 + form.new({ 100 + use category_id <- form.field("categoria", { 101 + form.parse_string 102 + |> form.check_not_empty 103 + }) 104 + use subcategory_id <- form.field("subcategoria", { form.parse_string }) 105 + use description <- form.field("descricao", { form.parse_string }) 106 + use location <- form.field("localizacao", { 107 + form.parse_list(form.parse_float) 108 + }) 109 + // HACK: That may be redundant, check with the frontend team 110 + // > @Kacaii 111 + use reference_point <- form.field("pontoReferencia", { form.parse_string }) 112 + use vehicle_code <- form.field("codigoViatura", { 113 + form.parse_string |> form.check_not_empty 114 + }) 115 + 116 + use participants_id <- form.field("participantes", { 117 + form.parse_list(form.parse_string) 118 + }) 119 + 120 + form.success(OccurrenceFormData( 121 + category_id:, 122 + subcategory_id:, 123 + description:, 124 + location:, 125 + reference_point:, 126 + vehicle_code:, 127 + participants_id:, 128 + )) 129 + }) 130 + } 131 + 132 + /// Handles occurrence registration form submission by validating form data, 133 + /// creating an occurrence record, and inserting it into the database with 134 + /// appropriate error responses. 135 + pub fn handle_form( 136 + request request: wisp.Request, 137 + ctx ctx: Context, 138 + ) -> wisp.Response { 139 + use form_data <- wisp.require_form(request) 140 + let form_result = 141 + occurence_form() 142 + |> form.add_values(form_data.values) 143 + |> form.run 144 + 145 + case form_result { 146 + Error(_) -> wisp.bad_request("Formulário Inválido") 147 + 148 + Ok(form_data) -> { 149 + let occurrence_result = new_occurrence(data: form_data, request:) 150 + case occurrence_result { 151 + Error(err) -> { 152 + case err { 153 + InvalidApplicantUUID(id) -> 154 + wisp.bad_request("ID de usuário inválido: " <> id) 155 + InvalidCategoryUUID(id) -> 156 + wisp.bad_request("ID de categoria inválido: " <> id) 157 + InvalidSubCategoryUUID(id) -> 158 + wisp.bad_request("ID de subcategoria inválido: " <> id) 159 + MissingCookie -> wisp.bad_request("Cookie Ausente") 160 + } 161 + } 162 + 163 + Ok(occurrence) -> { 164 + let insert_result = { 165 + sql.insert_new_occurence( 166 + ctx.conn, 167 + occurrence.applicant_id, 168 + occurrence.category_id, 169 + occurrence.subcategory_id, 170 + occurrence.description, 171 + occurrence.location, 172 + occurrence.reference_point, 173 + occurrence.vehicle_code, 174 + occurrence.participants_id, 175 + ) 176 + } 177 + 178 + case insert_result { 179 + Ok(_) -> 180 + wisp.created() 181 + |> wisp.set_body(wisp.Text("Ocorrência registrada com sucesso")) 182 + Error(err) -> { 183 + case err { 184 + pog.ConnectionUnavailable -> { 185 + let body = 186 + "Conexão com o banco de dados não disponível" 187 + |> wisp.Text 188 + 189 + wisp.internal_server_error() 190 + |> wisp.set_body(body) 191 + } 192 + pog.QueryTimeout -> { 193 + let body = 194 + "O banco de dados demorou muito para responder, talvez tenha perdido a conexão?" 195 + |> wisp.Text 196 + 197 + wisp.internal_server_error() 198 + |> wisp.set_body(body) 199 + } 200 + pog.ConstraintViolated(message:, constraint:, detail:) -> { 201 + let body = 202 + " 203 + 🐘 O banco de dados apresentou um erro 204 + 205 + Constraint: {{constraint}} 206 + Mensagem: {{message}} 207 + Detalhe: {{detail}} 208 + " 209 + |> string.replace("{{constraint}}", constraint) 210 + |> string.replace("{{message}}", message) 211 + |> string.replace("{{detail}}", detail) 212 + |> wisp.Text 213 + 214 + wisp.internal_server_error() 215 + |> wisp.set_body(body) 216 + } 217 + 218 + _ -> { 219 + let body = 220 + "Ocorreu um erro ao inserir o usuário no banco de dados" 221 + |> wisp.Text 222 + 223 + wisp.internal_server_error() 224 + |> wisp.set_body(body) 225 + } 226 + } 227 + } 228 + } 229 + } 230 + } 231 + } 232 + } 233 + } 234 + 235 + /// Represents possible errors that can occur during occurrence registration, 236 + /// including invalid UUID formats for applicant, category, or subcategory, 237 + /// and missing authentication cookie. 238 + type RegisterNewOccurrenceError { 239 + /// The provided applicant ID is not a valid UUID format 240 + InvalidApplicantUUID(String) 241 + /// The provided category ID is not a valid UUID format 242 + InvalidCategoryUUID(String) 243 + /// The provided subcategory ID is not a valid UUID format 244 + InvalidSubCategoryUUID(String) 245 + /// The required user authentication cookie is missing from the request 246 + MissingCookie 247 + }
+9 -4
src/app/routes/signup.gleam
··· 1 + //// Handler for user registration and account creation. 2 + //// 3 + //// It creates new user accounts by validating form data and inserting 4 + //// the user information into the database with proper password hashing. 5 + //// 6 + //// Passwords are hashed using Argon2 before storage and all sensitive 7 + //// operations are logged for audit purposes. 8 + 1 9 import app/sql 2 10 import app/web.{type Context} 3 11 import argus ··· 44 52 } 45 53 46 54 ///  Inserts a new `user_account` into the database 47 - pub fn handle_form_submission( 48 - request req: wisp.Request, 49 - ctx ctx: Context, 50 - ) -> wisp.Response { 55 + pub fn handle_form(request req: wisp.Request, ctx ctx: Context) -> wisp.Response { 51 56 use form_data <- wisp.require_form(req) 52 57 let form_result = 53 58 signup_form()
+148 -62
src/app/sql.gleam
··· 20 20 CountActiveBrigadesRow(count: Int) 21 21 } 22 22 23 - /// Runs the `count_active_brigades` query 24 - /// defined in `./src/app/sql/count_active_brigades.sql`. 23 + /// 󰆙 Counts the number of active brigades in the database. 25 24 /// 26 25 /// > 🐿️ This function was generated automatically using v4.4.1 of 27 26 /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). ··· 34 33 decode.success(CountActiveBrigadesRow(count:)) 35 34 } 36 35 37 - "SELECT COUNT(id) 36 + "-- 󰆙 Counts the number of active brigades in the database. 37 + SELECT COUNT(id) 38 38 FROM public.brigade 39 39 WHERE is_active = TRUE; 40 40 " ··· 51 51 /// 52 52 pub type GetBrigadeMembersRow { 53 53 GetBrigadeMembersRow( 54 + id: Uuid, 54 55 full_name: String, 55 56 role_name: Option(String), 56 57 description: Option(String), 57 58 ) 58 59 } 59 60 60 - /// Runs the `get_brigade_members` query 61 - /// defined in `./src/app/sql/get_brigade_members.sql`. 61 + ///  Find all members of a brigade 62 62 /// 63 63 /// > 🐿️ This function was generated automatically using v4.4.1 of 64 64 /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). ··· 68 68 arg_1: Uuid, 69 69 ) -> Result(pog.Returned(GetBrigadeMembersRow), pog.QueryError) { 70 70 let decoder = { 71 - use full_name <- decode.field(0, decode.string) 72 - use role_name <- decode.field(1, decode.optional(decode.string)) 73 - use description <- decode.field(2, decode.optional(decode.string)) 74 - decode.success(GetBrigadeMembersRow(full_name:, role_name:, description:)) 71 + use id <- decode.field(0, uuid_decoder()) 72 + use full_name <- decode.field(1, decode.string) 73 + use role_name <- decode.field(2, decode.optional(decode.string)) 74 + use description <- decode.field(3, decode.optional(decode.string)) 75 + decode.success(GetBrigadeMembersRow( 76 + id:, 77 + full_name:, 78 + role_name:, 79 + description:, 80 + )) 75 81 } 76 82 77 - "SELECT 83 + "--  Find all members of a brigade 84 + SELECT 85 + u.id, 78 86 u.full_name, 79 87 r.role_name, 80 88 r.description ··· 92 100 |> pog.execute(db) 93 101 } 94 102 95 - /// A row you get from running the `get_fellow_brigade_members` query 96 - /// defined in `./src/app/sql/get_fellow_brigade_members.sql`. 103 + /// A row you get from running the `get_crew_members` query 104 + /// defined in `./src/app/sql/get_crew_members.sql`. 97 105 /// 98 106 /// > 🐿️ This type definition was generated automatically using v4.4.1 of the 99 107 /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 100 108 /// 101 - pub type GetFellowBrigadeMembersRow { 102 - GetFellowBrigadeMembersRow( 109 + pub type GetCrewMembersRow { 110 + GetCrewMembersRow( 111 + id: Uuid, 103 112 full_name: String, 104 113 role_name: Option(String), 105 114 description: Option(String), 106 115 ) 107 116 } 108 117 109 - /// Runs the `get_fellow_brigade_members` query 110 - /// defined in `./src/app/sql/get_fellow_brigade_members.sql`. 118 + /// 󰢫 Retrieves detailed information about fellow brigade members 119 + /// for a given user, including their names and role details. 111 120 /// 112 121 /// > 🐿️ This function was generated automatically using v4.4.1 of 113 122 /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 114 123 /// 115 - pub fn get_fellow_brigade_members( 124 + pub fn get_crew_members( 116 125 db: pog.Connection, 117 126 arg_1: Uuid, 118 - ) -> Result(pog.Returned(GetFellowBrigadeMembersRow), pog.QueryError) { 127 + ) -> Result(pog.Returned(GetCrewMembersRow), pog.QueryError) { 119 128 let decoder = { 120 - use full_name <- decode.field(0, decode.string) 121 - use role_name <- decode.field(1, decode.optional(decode.string)) 122 - use description <- decode.field(2, decode.optional(decode.string)) 123 - decode.success(GetFellowBrigadeMembersRow( 124 - full_name:, 125 - role_name:, 126 - description:, 127 - )) 129 + use id <- decode.field(0, uuid_decoder()) 130 + use full_name <- decode.field(1, decode.string) 131 + use role_name <- decode.field(2, decode.optional(decode.string)) 132 + use description <- decode.field(3, decode.optional(decode.string)) 133 + decode.success(GetCrewMembersRow(id:, full_name:, role_name:, description:)) 128 134 } 129 135 130 - "SELECT 136 + "-- 󰢫 Retrieves detailed information about fellow brigade members 137 + -- for a given user, including their names and role details. 138 + SELECT 139 + u.id, 131 140 u.full_name, 132 141 r.role_name, 133 142 r.description 134 - FROM QUERY_FELLOW_BRIGADE_MEMBERS_ID($1) AS fellow_members (id) 143 + FROM QUERY_FELLOW_BRIGADE_MEMBERS_ID($1) AS crew_members (id) 135 144 INNER JOIN 136 145 public.user_account AS u 137 - ON fellow_members.id = u.id 146 + ON crew_members.id = u.id 138 147 LEFT JOIN 139 148 public.user_role AS r 140 149 ON u.role_id = r.id; ··· 155 164 GetLoginTokenRow(id: Uuid, password_hash: String) 156 165 } 157 166 158 - /// Runs the `get_login_token` query 159 - /// defined in `./src/app/sql/get_login_token.sql`. 167 + ///  Retrieves a user's ID and password hash from their registration 168 + /// number for authentication purposes. 160 169 /// 161 170 /// > 🐿️ This function was generated automatically using v4.4.1 of 162 171 /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). ··· 171 180 decode.success(GetLoginTokenRow(id:, password_hash:)) 172 181 } 173 182 174 - "SELECT 183 + "--  Retrieves a user's ID and password hash from their registration 184 + -- number for authentication purposes. 185 + SELECT 175 186 u.id, 176 187 u.password_hash 177 188 FROM public.user_account AS u ··· 191 202 /// 192 203 pub type GetOccurencesByApplicantRow { 193 204 GetOccurencesByApplicantRow( 205 + id: Uuid, 194 206 description: Option(String), 195 207 category: Option(String), 196 208 subcategory: Option(String), 197 209 created_at: Option(Timestamp), 198 210 resolved_at: Option(Timestamp), 199 211 location: List(Float), 200 - reference_point: String, 201 - loss_percentage: Option(Float), 212 + reference_point: Option(String), 202 213 ) 203 214 } 204 215 205 - /// Runs the `get_occurences_by_applicant` query 206 - /// defined in `./src/app/sql/get_occurences_by_applicant.sql`. 216 + ///  Retrieves all occurrences associated with a user, 217 + /// including detailed category information and resolution status. 207 218 /// 208 219 /// > 🐿️ This function was generated automatically using v4.4.1 of 209 220 /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). ··· 213 224 arg_1: Uuid, 214 225 ) -> Result(pog.Returned(GetOccurencesByApplicantRow), pog.QueryError) { 215 226 let decoder = { 216 - use description <- decode.field(0, decode.optional(decode.string)) 217 - use category <- decode.field(1, decode.optional(decode.string)) 218 - use subcategory <- decode.field(2, decode.optional(decode.string)) 219 - use created_at <- decode.field(3, decode.optional(pog.timestamp_decoder())) 220 - use resolved_at <- decode.field(4, decode.optional(pog.timestamp_decoder())) 221 - use location <- decode.field(5, decode.list(decode.float)) 222 - use reference_point <- decode.field(6, decode.string) 223 - use loss_percentage <- decode.field( 224 - 7, 225 - decode.optional(pog.numeric_decoder()), 226 - ) 227 + use id <- decode.field(0, uuid_decoder()) 228 + use description <- decode.field(1, decode.optional(decode.string)) 229 + use category <- decode.field(2, decode.optional(decode.string)) 230 + use subcategory <- decode.field(3, decode.optional(decode.string)) 231 + use created_at <- decode.field(4, decode.optional(pog.timestamp_decoder())) 232 + use resolved_at <- decode.field(5, decode.optional(pog.timestamp_decoder())) 233 + use location <- decode.field(6, decode.list(decode.float)) 234 + use reference_point <- decode.field(7, decode.optional(decode.string)) 227 235 decode.success(GetOccurencesByApplicantRow( 236 + id:, 228 237 description:, 229 238 category:, 230 239 subcategory:, ··· 232 241 resolved_at:, 233 242 location:, 234 243 reference_point:, 235 - loss_percentage:, 236 244 )) 237 245 } 238 246 239 - "SELECT 247 + "--  Retrieves all occurrences associated with a user, 248 + -- including detailed category information and resolution status. 249 + SELECT 250 + o.id, 240 251 o.description, 241 252 oc_cat.category_name AS category, 242 253 sub_cat.category_name AS subcategory, 243 254 o.created_at, 244 255 o.resolved_at, 245 256 o.location, 246 - o.reference_point, 247 - o.loss_percentage 257 + o.reference_point 248 258 FROM public.query_all_ocurrences_by_user_id($1) AS oc_list (id) 249 259 INNER JOIN public.occurrence AS o 250 260 ON oc_list.id = o.id ··· 269 279 GetUserIdByRegistrationRow(id: Uuid) 270 280 } 271 281 272 - /// Runs the `get_user_id_by_registration` query 273 - /// defined in `./src/app/sql/get_user_id_by_registration.sql`. 282 + ///  Retrieves a user's ID from their registration number. 274 283 /// 275 284 /// > 🐿️ This function was generated automatically using v4.4.1 of 276 285 /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). ··· 284 293 decode.success(GetUserIdByRegistrationRow(id:)) 285 294 } 286 295 287 - "SELECT u.id 296 + "--  Retrieves a user's ID from their registration number. 297 + SELECT u.id 288 298 FROM public.user_account AS u 289 299 WHERE u.registration = $1; 290 300 " ··· 294 304 |> pog.execute(db) 295 305 } 296 306 297 - /// Runs the `insert_new_user` query 298 - /// defined in `./src/app/sql/insert_new_user.sql`. 307 + /// A row you get from running the `get_user_name` query 308 + /// defined in `./src/app/sql/get_user_name.sql`. 309 + /// 310 + /// > 🐿️ This type definition was generated automatically using v4.4.1 of the 311 + /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 312 + /// 313 + pub type GetUserNameRow { 314 + GetUserNameRow(full_name: String) 315 + } 316 + 317 + ///  Retrieves a user's full name by their user ID. 318 + /// 319 + /// > 🐿️ This function was generated automatically using v4.4.1 of 320 + /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 321 + /// 322 + pub fn get_user_name( 323 + db: pog.Connection, 324 + arg_1: Uuid, 325 + ) -> Result(pog.Returned(GetUserNameRow), pog.QueryError) { 326 + let decoder = { 327 + use full_name <- decode.field(0, decode.string) 328 + decode.success(GetUserNameRow(full_name:)) 329 + } 330 + 331 + "--  Retrieves a user's full name by their user ID. 332 + SELECT u.full_name 333 + FROM public.user_account AS u 334 + WHERE u.id = $1; 335 + " 336 + |> pog.query 337 + |> pog.parameter(pog.text(uuid.to_string(arg_1))) 338 + |> pog.returning(decoder) 339 + |> pog.execute(db) 340 + } 341 + 342 + ///  Inserts a new occurrence into the database 343 + /// 344 + /// > 🐿️ This function was generated automatically using v4.4.1 of 345 + /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 346 + /// 347 + pub fn insert_new_occurence( 348 + db: pog.Connection, 349 + arg_1: Uuid, 350 + arg_2: Uuid, 351 + arg_3: Uuid, 352 + arg_4: String, 353 + arg_5: List(Float), 354 + arg_6: String, 355 + arg_7: String, 356 + arg_8: List(Uuid), 357 + ) -> Result(pog.Returned(Nil), pog.QueryError) { 358 + let decoder = decode.map(decode.dynamic, fn(_) { Nil }) 359 + 360 + "--  Inserts a new occurrence into the database 361 + INSERT INTO public.occurrence ( 362 + applicant_id, 363 + category_id, 364 + subcategory_id, 365 + description, 366 + location, 367 + reference_point, 368 + vehicle_code, 369 + participants_id 370 + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8); 371 + " 372 + |> pog.query 373 + |> pog.parameter(pog.text(uuid.to_string(arg_1))) 374 + |> pog.parameter(pog.text(uuid.to_string(arg_2))) 375 + |> pog.parameter(pog.text(uuid.to_string(arg_3))) 376 + |> pog.parameter(pog.text(arg_4)) 377 + |> pog.parameter(pog.array(fn(value) { pog.float(value) }, arg_5)) 378 + |> pog.parameter(pog.text(arg_6)) 379 + |> pog.parameter(pog.text(arg_7)) 380 + |> pog.parameter( 381 + pog.array(fn(value) { pog.text(uuid.to_string(value)) }, arg_8), 382 + ) 383 + |> pog.returning(decoder) 384 + |> pog.execute(db) 385 + } 386 + 387 + ///  Inserts a new user into the database 299 388 /// 300 389 /// > 🐿️ This function was generated automatically using v4.4.1 of 301 390 /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). ··· 310 399 ) -> Result(pog.Returned(Nil), pog.QueryError) { 311 400 let decoder = decode.map(decode.dynamic, fn(_) { Nil }) 312 401 313 - "INSERT INTO public.user_account ( 402 + "--  Inserts a new user into the database 403 + INSERT INTO public.user_account ( 314 404 full_name, 315 405 registration, 316 406 phone, 317 407 email, 318 408 password_hash 319 409 ) VALUES ( 320 - $1, 321 - $2, 322 - $3, 323 - $4, 324 - $5 410 + $1, $2, $3, $4, $5 325 411 ) 326 412 " 327 413 |> pog.query
+1
src/app/sql/count_active_brigades.sql
··· 1 + -- 󰆙 Counts the number of active brigades in the database. 1 2 SELECT COUNT(id) 2 3 FROM public.brigade 3 4 WHERE is_active = TRUE;
+2
src/app/sql/get_brigade_members.sql
··· 1 + --  Find all members of a brigade 1 2 SELECT 3 + u.id, 2 4 u.full_name, 3 5 r.role_name, 4 6 r.description
+14
src/app/sql/get_crew_members.sql
··· 1 + -- 󰢫 Retrieves detailed information about fellow brigade members 2 + -- for a given user, including their names and role details. 3 + SELECT 4 + u.id, 5 + u.full_name, 6 + r.role_name, 7 + r.description 8 + FROM QUERY_FELLOW_BRIGADE_MEMBERS_ID($1) AS crew_members (id) 9 + INNER JOIN 10 + public.user_account AS u 11 + ON crew_members.id = u.id 12 + LEFT JOIN 13 + public.user_role AS r 14 + ON u.role_id = r.id;
-11
src/app/sql/get_fellow_brigade_members.sql
··· 1 - SELECT 2 - u.full_name, 3 - r.role_name, 4 - r.description 5 - FROM QUERY_FELLOW_BRIGADE_MEMBERS_ID($1) AS fellow_members (id) 6 - INNER JOIN 7 - public.user_account AS u 8 - ON fellow_members.id = u.id 9 - LEFT JOIN 10 - public.user_role AS r 11 - ON u.role_id = r.id;
+2
src/app/sql/get_login_token.sql
··· 1 + --  Retrieves a user's ID and password hash from their registration 2 + -- number for authentication purposes. 1 3 SELECT 2 4 u.id, 3 5 u.password_hash
+4 -2
src/app/sql/get_occurences_by_applicant.sql
··· 1 + --  Retrieves all occurrences associated with a user, 2 + -- including detailed category information and resolution status. 1 3 SELECT 4 + o.id, 2 5 o.description, 3 6 oc_cat.category_name AS category, 4 7 sub_cat.category_name AS subcategory, 5 8 o.created_at, 6 9 o.resolved_at, 7 10 o.location, 8 - o.reference_point, 9 - o.loss_percentage 11 + o.reference_point 10 12 FROM public.query_all_ocurrences_by_user_id($1) AS oc_list (id) 11 13 INNER JOIN public.occurrence AS o 12 14 ON oc_list.id = o.id
+1
src/app/sql/get_user_id_by_registration.sql
··· 1 + --  Retrieves a user's ID from their registration number. 1 2 SELECT u.id 2 3 FROM public.user_account AS u 3 4 WHERE u.registration = $1;
+4
src/app/sql/get_user_name.sql
··· 1 + --  Retrieves a user's full name by their user ID. 2 + SELECT u.full_name 3 + FROM public.user_account AS u 4 + WHERE u.id = $1;
+11
src/app/sql/insert_new_occurence.sql
··· 1 + --  Inserts a new occurrence into the database 2 + INSERT INTO public.occurrence ( 3 + applicant_id, 4 + category_id, 5 + subcategory_id, 6 + description, 7 + location, 8 + reference_point, 9 + vehicle_code, 10 + participants_id 11 + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8);
+2 -5
src/app/sql/insert_new_user.sql
··· 1 + --  Inserts a new user into the database 1 2 INSERT INTO public.user_account ( 2 3 full_name, 3 4 registration, ··· 5 6 email, 6 7 password_hash 7 8 ) VALUES ( 8 - $1, 9 - $2, 10 - $3, 11 - $4, 12 - $5 9 + $1, $2, $3, $4, $5 13 10 )
+21
src/app/web.gleam
··· 1 + //// Web application context and middleware configuration. 2 + //// 3 + //// This module defines the core web infrastructure including: 4 + //// - The `Context` type that holds application dependencies for request handlers 5 + //// - Middleware pipeline for request processing 6 + //// - CORS configuration for cross-origin requests 7 + //// - Logger configuration for application logging 8 + //// 9 + //// ## Middleware Pipeline 10 + //// The middleware function applies the following processing to each request: 11 + //// - HTTP method overriding (for REST clients) 12 + //// - Request logging 13 + //// - Crash recovery and error handling 14 + //// - HEAD request normalization 15 + //// - CORS headers 16 + //// - Static file serving from `/static` path 17 + //// 18 + //// ## CORS Configuration 19 + //// Currently configured to allow requests from `http://localhost:5173` 20 + //// with GET and POST methods enabled. 21 + 1 22 import cors_builder as cors 2 23 import gleam/http 3 24 import glight