···8899## Routes
10101111-| Route | Description | Method |
1212-| ---------------------------------------------- | ---------------------------------------- | ----------- |
1313-| /api/user/signup | Register a new user account | POST (Form) |
1414-| /api/user/login | Login with your user account | POST (Form) |
1515-| /api/user/get_occurrences/{user_id} | Get all occurrences applied by this user | GET |
1616-| /api/user/get_fellow_brigade_members/{user_id} | List fellow brigade members of this user | GET |
1717-| /api/brigade/get_members/{brigade_id} | List brigade members | GET |
1111+| Route | Description | Method |
1212+| --------------------------- | ---------------------------------------- | ----------- |
1313+| /api/user/signup | Register a new user account | POST (Form) |
1414+| /api/user/login | Login with your user account | POST (Form) |
1515+| /api/occurrence/new | Register new occurrence | POST (Form) |
1616+| /api/user/{id}/occurrences | Get all occurrences applied by this user | GET |
1717+| /api/user/{id}/crew_members | List fellow brigade members of this user | GET |
1818+| /api/brigade/{id}/members | List brigade members | GET |
18191920## Entity RelationShip Diagram
2021
···77DROP INDEX IF EXISTS public.idx_user_registration;
88DROP INDEX IF EXISTS public.idx_user_id;
99DROP INDEX IF EXISTS public.idx_occurrence_id;
1010+DROP INDEX IF EXISTS public.idx_occurrence_brigade_member_user_id;
1111+DROP INDEX IF EXISTS public.idx_occurrence_brigade_member_occurrence_id;
10121113-- pgt-ignore-start lint/safety/banDropTable: We are resetting the Database
1214DROP TABLE IF EXISTS public.occurrence;
1315DROP TABLE IF EXISTS public.occurrence_category;
1616+DROP TABLE IF EXISTS public.occurrence_brigade_member;
1417DROP TABLE IF EXISTS public.brigade_membership;
1518DROP TABLE IF EXISTS public.brigade;
1619DROP TABLE IF EXISTS public.user_account;
···41444245CREATE INDEX IF NOT EXISTS idx_user_registration
4346ON public.user_account (registration);
4444-4545-CREATE INDEX IF NOT EXISTS idx_user_id
4646-ON public.user_account (id);
47474848CREATE TABLE IF NOT EXISTS public.brigade (
4949 id UUID PRIMARY KEY DEFAULT UUIDV7(),
···8585 ON UPDATE CASCADE ON DELETE SET NULL DEFAULT NULL,
8686 description TEXT,
8787 location POINT NOT NULL,
8888- reference_point TEXT NOT NULL,
8989- loss_percentage NUMERIC(2),
8888+ reference_point TEXT,
8989+ vehicle_code TEXT NOT NULL,
9090+ participants_id UUID [],
9091 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
9192 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
9293 resolved_at TIMESTAMP DEFAULT NULL
···9596CREATE INDEX IF NOT EXISTS idx_occurrence_applicant_id
9697ON public.occurrence (applicant_id);
97989898-CREATE INDEX IF NOT EXISTS idx_occurrence_id
9999-ON public.occurrence (id);
9999+CREATE TABLE IF NOT EXISTS public.occurrence_brigade_member (
100100+ id UUID PRIMARY KEY DEFAULT UUIDV7(),
101101+ user_id UUID REFERENCES public.user_account (id)
102102+ ON UPDATE CASCADE,
103103+ occurrence_id UUID REFERENCES public.user_account (id)
104104+ ON UPDATE CASCADE
105105+);
106106+107107+CREATE INDEX IF NOT EXISTS idx_occurrence_brigade_member_user_id
108108+ON public.occurrence_brigade_member (user_id);
109109+110110+CREATE INDEX IF NOT EXISTS idx_occurrence_brigade_member_occurrence_id
111111+ON public.occurrence_brigade_member (occurrence_id);
100112101113COMMIT;
+21
priv/sql/create/triggers.sql
···11+DROP FUNCTION IF EXISTS public.dump_occurrence_participants;
22+DROP TRIGGER IF EXISTS tgr_insert_member_participation ON occurrence;
33+44+--
55+CREATE OR REPLACE FUNCTION public.dump_occurrence_participants()
66+RETURNS TRIGGER AS $$
77+BEGIN
88+ INSERT INTO public.occurrence_brigade_member (brigade_id, user_id)
99+ SELECT NEW.id, unnest(participants_id)
1010+ FROM public.occurrence AS oc
1111+ WHERE id = NEW.id
1212+ AND participants_id IS NOT NULL;
1313+1414+ RETURN NEW;
1515+END;
1616+$$ LANGUAGE plpgsql;
1717+1818+CREATE OR REPLACE TRIGGER tgr_insert_member_participation
1919+AFTER INSERT ON public.occurrence
2020+FOR EACH ROW
2121+EXECUTE FUNCTION public.dump_occurrence_participants();
+20-1
src/app.gleam
···11+//// A web application built with the Wisp framework.
22+////
33+//// This module is the main entry point for the application. It is responsible for:
44+//// - Configuring the application's dependencies (database, HTTP server)
55+//// - Reading necessary environment variables
66+//// - Starting the supervision tree that manages the application's processes
77+////
88+//// ## Environment Variables
99+//// - `DATABASE_URL`: The connection URI for the PostgreSQL database
1010+//// - `COOKIE_TOKEN`: The secret key used for signing and encrypting cookies
1111+////
1212+//// ## Architecture
1313+//// The application uses a supervisor to manage two main processes:
1414+//// 1. A PostgreSQL database connection pool using Pog
1515+//// 2. An HTTP server using Mist (with Wisp handling the web layer)
1616+////
1717+//// The supervision strategy is `OneForOne`, meaning if either process fails,
1818+//// only that specific process will be restarted, leaving the other unaffected.
1919+120import app/router
221import app/web.{Context}
322import envoy
···6382 |> mist.port(8000)
6483 }
65846666- supervisor.new(supervisor.RestForOne)
8585+ supervisor.new(supervisor.OneForOne)
6786 |> supervisor.add(pog_pool_child)
6887 |> supervisor.add(mist.supervised(mist_pool_child))
6988 |> supervisor.start
+32-12
src/app/router.gleam
···11+//// Application router that maps HTTP requests to handler functions.
22+////
33+//// This module defines the URL routing table for the application's API endpoints.
44+//// It uses path-based routing to delegate requests to specific handler modules
55+//// for processing.
66+////
77+//// All requests are processed through the web middleware pipeline before routing.
88+//// Unmatched routes return a 404 Not Found response.
99+110import app/routes/get_brigade_members
22-import app/routes/get_fellow_brigade_members
1111+import app/routes/get_crew_members
312import app/routes/get_ocurrences_by_applicant
413import app/routes/login
1414+import app/routes/register_new_occurrence
515import app/routes/signup
616import app/web.{type Context}
1717+import gleam/http
718import wisp
819920/// Handle the incoming HTTP Requests
1021pub fn handle_request(request: wisp.Request, ctx: Context) -> wisp.Response {
1122 use request <- web.middleware(request: request, context: ctx)
12231313- case wisp.path_segments(request) {
1414- [] -> wisp.ok()
1515- ["api", "user", "signup"] -> signup.handle_form_submission(request:, ctx:)
1616- ["api", "user", "login"] -> login.handle_form_submission(request:, ctx:)
1717- ["api", "user", "get_occurences", user_id] ->
1818- get_ocurrences_by_applicant.handle_request(request:, ctx:, user_id:)
2424+ case request.method, wisp.path_segments(request) {
2525+ // Authorization routes
2626+ http.Post, ["api", "user", "signup"] -> signup.handle_form(request:, ctx:)
2727+ http.Post, ["api", "user", "login"] -> login.handle_form(request:, ctx:)
19282020- ["api", "brigade", "get_members", brigade_id] ->
2121- get_brigade_members.handle_request(request:, ctx:, brigade_id:)
2222- ["api", "user", "get_fellow_members", user_id] ->
2323- get_fellow_brigade_members.handle_request(request:, ctx:, user_id:)
2929+ // User data routes
3030+ http.Get, ["api", "user", id, "occurrences"] ->
3131+ get_ocurrences_by_applicant.handle_request(request:, ctx:, id:)
3232+ http.Get, ["api", "user", id, "crew_members"] ->
3333+ get_crew_members.handle_request(request:, ctx:, id:)
24342525- _ -> wisp.not_found()
3535+ // Occurrence routes
3636+ http.Post, ["api", "occurence", "new"] ->
3737+ register_new_occurrence.handle_form(request:, ctx:)
3838+3939+ // Brigade routes
4040+ http.Get, ["api", "brigade", id, "members"] ->
4141+ get_brigade_members.handle_request(request:, ctx:, id:)
4242+4343+ // Fallback routes
4444+ _, [] -> wisp.ok()
4545+ _, _ -> wisp.not_found()
2646 }
2747}
+14-2
src/app/routes/get_brigade_members.gleam
···11+//// Handler for retrieving members of a specific fire brigade.
22+////
33+//// It returns a list of members belonging to the specified brigade, including
44+//// their id, full name, role, and description.
55+16import app/sql
27import app/web.{type Context}
38import gleam/http
···813import wisp
914import youid/uuid
10151616+/// Retrieves all members of a specific fire brigade from the database
1717+/// and returns them as formatted JSON data.
1118pub fn handle_request(
1219 request req: wisp.Request,
1320 ctx ctx: Context,
1414- brigade_id brigade_id: String,
2121+ id brigade_id: String,
1522) -> wisp.Response {
1623 use <- wisp.require_method(req, http.Get)
1724···4956fn get_brigade_members_row_to_json(
5057 get_brigade_members_row: sql.GetBrigadeMembersRow,
5158) -> json.Json {
5252- let sql.GetBrigadeMembersRow(full_name:, description:, role_name:) =
5959+ let sql.GetBrigadeMembersRow(id:, full_name:, description:, role_name:) =
5360 get_brigade_members_row
5461 json.object([
6262+ #("id", json.string(uuid.to_string(id))),
5563 #("full_name", json.string(full_name)),
5664 #("role_name", json.string(option.unwrap(role_name, ""))),
5765 #("description", json.string(option.unwrap(description, ""))),
5866 ])
5967}
60686969+/// Represents possible errors that can occur when retrieving brigade members
7070+/// from the database
6171type GetBrigadeMembersError {
7272+ /// The provided brigade ID is not a valid UUID format
6273 InvalidUUID
7474+ /// An error occurred while accessing the database to retrieve brigade members
6375 DataBaseError
6476}
···11+//// Handler for retrieving members from the same brigade as a given user.
22+13import app/sql
24import app/web.{type Context}
35import gleam/http
···810import wisp
911import youid/uuid
10121313+/// Retrieves all crew members or brigade members associated with a specific user
1414+/// from the database and returns them as formatted JSON data.
1115pub fn handle_request(
1216 request req: wisp.Request,
1317 ctx ctx: Context,
1414- user_id user_id: String,
1818+ id user_id: String,
1519) -> wisp.Response {
1620 use <- wisp.require_method(req, http.Get)
1721···2125 |> result.replace_error(InvalidUUID),
2226 )
2327 use returned <- result.try(
2424- sql.get_fellow_brigade_members(ctx.conn, user_uuid)
2828+ sql.get_crew_members(ctx.conn, user_uuid)
2529 |> result.replace_error(DataBaseError),
2630 )
2731 let fellow_members_list = {
2832 use fellow_brigade_member <- list.map(returned.rows)
2929- get_fellow_brigade_members_row_to_json(fellow_brigade_member)
3333+ get_crew_members_row_to_json(fellow_brigade_member)
3034 }
31353236 Ok(json.preprocessed_array(fellow_members_list))
···5357 InvalidUUID
5458}
55595656-fn get_fellow_brigade_members_row_to_json(
5757- get_fellow_brigade_members_row: sql.GetFellowBrigadeMembersRow,
6060+fn get_crew_members_row_to_json(
6161+ get_crew_members_row: sql.GetCrewMembersRow,
5862) -> json.Json {
5959- let sql.GetFellowBrigadeMembersRow(full_name:, role_name:, description:) =
6060- get_fellow_brigade_members_row
6363+ let sql.GetCrewMembersRow(id:, full_name:, role_name:, description:) =
6464+ get_crew_members_row
6165 json.object([
6666+ #("id", json.string(uuid.to_string(id))),
6267 #("full_name", json.string(full_name)),
6368 #("role_name", json.string(option.unwrap(role_name, ""))),
6469 #("description", json.string(option.unwrap(description, ""))),
+15-7
src/app/routes/get_ocurrences_by_applicant.gleam
···11+//// Handler for retrieving occurrences reported by a specific applicant.
22+////
33+//// It returns a list of occurrences (incidents/reports) that were submitted
44+//// by the specified user, including detailed information about each occurrence.
55+16import app/sql
27import app/web
38import gleam/float
···1116import wisp
1217import youid/uuid
13181919+/// Fetches all occurrences/applications associated with a specific user
2020+/// from the database and returns them as JSON.
1421pub fn handle_request(
1522 request request: wisp.Request,
1623 ctx ctx: web.Context,
1717- user_id user_id: String,
2424+ id user_id: String,
1825) -> wisp.Response {
1926 use <- wisp.require_method(request, http.Get)
2027···5966 }
6067}
61686969+/// Represents possible errors that can occur during the search
7070+/// including invalid UUID formats for applicant
6271type GetOccurrencesByApplicantError {
7272+ /// The provided applicant ID is not a valid UUID format
6373 InvalidUUID
7474+ /// An Error occurred when querying the database
6475 DatabaseError(pog.QueryError)
6576}
6677···6879 get_occurences_by_applicant_row: sql.GetOccurencesByApplicantRow,
6980) -> json.Json {
7081 let sql.GetOccurencesByApplicantRow(
8282+ id:,
7183 description:,
7284 category:,
7385 subcategory:,
···7587 resolved_at:,
7688 location:,
7789 reference_point:,
7878- loss_percentage:,
7990 ) = get_occurences_by_applicant_row
8091 json.object([
9292+ #("id", json.string(uuid.to_string(id))),
8193 #("description", case description {
8294 option.None -> json.null()
8395 option.Some(value) -> json.string(value)
···107119 }
108120 }),
109121 #("location", json.array(location, json.float)),
110110- #("reference_point", json.string(reference_point)),
111111- #("loss_percentage", case loss_percentage {
112112- option.None -> json.null()
113113- option.Some(value) -> json.float(value)
114114- }),
122122+ #("reference_point", json.string(option.unwrap(reference_point, ""))),
115123 ])
116124}
+8-4
src/app/routes/login.gleam
···11+//// Handler for user authentication and login.
22+////
33+//// Uses signed cookies to prevent tampering and logs all login attempts.
44+15import app/sql
26import app/web.{type Context}
37import argus
···2933 })
3034}
31353232-/// Verifies if a user is registred
3333-pub fn handle_form_submission(request request: wisp.Request, ctx ctx: Context) {
3434- use form_data <- wisp.require_form(request)
3636+/// Handles user login authentication and session management
3737+pub fn handle_form(request cookie_user_uuid: wisp.Request, ctx ctx: Context) {
3838+ use form_data <- wisp.require_form(cookie_user_uuid)
3539 let form_result =
3640 login_form()
3741 |> form.add_values(form_data.values)
···4953 // Store UUID cookie
5054 wisp.set_cookie(
5155 response: wisp.ok(),
5252- request: request,
5656+ request: cookie_user_uuid,
5357 name: cookie_name,
5458 value: uuid.to_string(user_uuid),
5559 security: wisp.Signed,
+247
src/app/routes/register_new_occurrence.gleam
···11+//// Processes occurrence registration form data, validates inputs, and creates
22+//// a new occurrence record in the database.
33+44+import app/sql
55+import app/web.{type Context}
66+import formal/form
77+import gleam/list
88+import gleam/result
99+import gleam/string
1010+import pog
1111+import wisp
1212+import youid/uuid
1313+1414+const cookie_user_id = "USER_ID"
1515+1616+/// Raw form data submitted for creating an occurrence, with all IDs as strings
1717+pub opaque type OccurrenceFormData {
1818+ OccurrenceFormData(
1919+ category_id: String,
2020+ subcategory_id: String,
2121+ description: String,
2222+ location: List(Float),
2323+ reference_point: String,
2424+ vehicle_code: String,
2525+ participants_id: List(String),
2626+ )
2727+}
2828+2929+/// Validated occurrence data with all IDs converted to UUIDs,
3030+/// ready for database insertion
3131+pub opaque type Occurrence {
3232+ Occurrence(
3333+ applicant_id: uuid.Uuid,
3434+ category_id: uuid.Uuid,
3535+ subcategory_id: uuid.Uuid,
3636+ description: String,
3737+ location: List(Float),
3838+ reference_point: String,
3939+ vehicle_code: String,
4040+ participants_id: List(uuid.Uuid),
4141+ )
4242+}
4343+4444+/// Validates and constructs an Occurrence from form data by converting string
4545+/// IDs to UUIDs and extracting the applicant ID from request cookies.
4646+fn new_occurrence(
4747+ data data: OccurrenceFormData,
4848+ request request: wisp.Request,
4949+) -> Result(Occurrence, RegisterNewOccurrenceError) {
5050+ use category_id <- result.try(
5151+ uuid.from_string(data.category_id)
5252+ |> result.replace_error(InvalidCategoryUUID(data.category_id)),
5353+ )
5454+ use subcategory_id <- result.try(
5555+ uuid.from_string(data.subcategory_id)
5656+ |> result.replace_error(InvalidSubCategoryUUID(data.subcategory_id)),
5757+ )
5858+5959+ use participants_id <- result.try({
6060+ use id_string <- list.try_map(data.participants_id)
6161+ uuid.from_string(id_string)
6262+ |> result.replace_error(InvalidApplicantUUID(id_string))
6363+ })
6464+6565+ use applicant_id <- result.try(get_user_id(request))
6666+6767+ Ok(Occurrence(
6868+ applicant_id:,
6969+ category_id:,
7070+ subcategory_id:,
7171+ description: data.description,
7272+ location: data.location,
7373+ reference_point: data.reference_point,
7474+ vehicle_code: data.vehicle_code,
7575+ participants_id:,
7676+ ))
7777+}
7878+7979+/// Extracts and validates the user ID from a signed cookie in the request,
8080+/// returning it as a UUID.
8181+fn get_user_id(
8282+ request: wisp.Request,
8383+) -> Result(uuid.Uuid, RegisterNewOccurrenceError) {
8484+ use user_id_string <- result.try(
8585+ wisp.get_cookie(request:, name: cookie_user_id, security: wisp.Signed)
8686+ |> result.replace_error(MissingCookie),
8787+ )
8888+8989+ use user_uuid <- result.try(
9090+ uuid.from_string(user_id_string)
9191+ |> result.replace_error(InvalidApplicantUUID(user_id_string)),
9292+ )
9393+9494+ Ok(user_uuid)
9595+}
9696+9797+/// A form that decodes the `Occurrence` type
9898+fn occurence_form() -> form.Form(OccurrenceFormData) {
9999+ form.new({
100100+ use category_id <- form.field("categoria", {
101101+ form.parse_string
102102+ |> form.check_not_empty
103103+ })
104104+ use subcategory_id <- form.field("subcategoria", { form.parse_string })
105105+ use description <- form.field("descricao", { form.parse_string })
106106+ use location <- form.field("localizacao", {
107107+ form.parse_list(form.parse_float)
108108+ })
109109+ // HACK: That may be redundant, check with the frontend team
110110+ // > @Kacaii
111111+ use reference_point <- form.field("pontoReferencia", { form.parse_string })
112112+ use vehicle_code <- form.field("codigoViatura", {
113113+ form.parse_string |> form.check_not_empty
114114+ })
115115+116116+ use participants_id <- form.field("participantes", {
117117+ form.parse_list(form.parse_string)
118118+ })
119119+120120+ form.success(OccurrenceFormData(
121121+ category_id:,
122122+ subcategory_id:,
123123+ description:,
124124+ location:,
125125+ reference_point:,
126126+ vehicle_code:,
127127+ participants_id:,
128128+ ))
129129+ })
130130+}
131131+132132+/// Handles occurrence registration form submission by validating form data,
133133+/// creating an occurrence record, and inserting it into the database with
134134+/// appropriate error responses.
135135+pub fn handle_form(
136136+ request request: wisp.Request,
137137+ ctx ctx: Context,
138138+) -> wisp.Response {
139139+ use form_data <- wisp.require_form(request)
140140+ let form_result =
141141+ occurence_form()
142142+ |> form.add_values(form_data.values)
143143+ |> form.run
144144+145145+ case form_result {
146146+ Error(_) -> wisp.bad_request("Formulário Inválido")
147147+148148+ Ok(form_data) -> {
149149+ let occurrence_result = new_occurrence(data: form_data, request:)
150150+ case occurrence_result {
151151+ Error(err) -> {
152152+ case err {
153153+ InvalidApplicantUUID(id) ->
154154+ wisp.bad_request("ID de usuário inválido: " <> id)
155155+ InvalidCategoryUUID(id) ->
156156+ wisp.bad_request("ID de categoria inválido: " <> id)
157157+ InvalidSubCategoryUUID(id) ->
158158+ wisp.bad_request("ID de subcategoria inválido: " <> id)
159159+ MissingCookie -> wisp.bad_request("Cookie Ausente")
160160+ }
161161+ }
162162+163163+ Ok(occurrence) -> {
164164+ let insert_result = {
165165+ sql.insert_new_occurence(
166166+ ctx.conn,
167167+ occurrence.applicant_id,
168168+ occurrence.category_id,
169169+ occurrence.subcategory_id,
170170+ occurrence.description,
171171+ occurrence.location,
172172+ occurrence.reference_point,
173173+ occurrence.vehicle_code,
174174+ occurrence.participants_id,
175175+ )
176176+ }
177177+178178+ case insert_result {
179179+ Ok(_) ->
180180+ wisp.created()
181181+ |> wisp.set_body(wisp.Text("Ocorrência registrada com sucesso"))
182182+ Error(err) -> {
183183+ case err {
184184+ pog.ConnectionUnavailable -> {
185185+ let body =
186186+ "Conexão com o banco de dados não disponível"
187187+ |> wisp.Text
188188+189189+ wisp.internal_server_error()
190190+ |> wisp.set_body(body)
191191+ }
192192+ pog.QueryTimeout -> {
193193+ let body =
194194+ "O banco de dados demorou muito para responder, talvez tenha perdido a conexão?"
195195+ |> wisp.Text
196196+197197+ wisp.internal_server_error()
198198+ |> wisp.set_body(body)
199199+ }
200200+ pog.ConstraintViolated(message:, constraint:, detail:) -> {
201201+ let body =
202202+ "
203203+ 🐘 O banco de dados apresentou um erro
204204+205205+ Constraint: {{constraint}}
206206+ Mensagem: {{message}}
207207+ Detalhe: {{detail}}
208208+ "
209209+ |> string.replace("{{constraint}}", constraint)
210210+ |> string.replace("{{message}}", message)
211211+ |> string.replace("{{detail}}", detail)
212212+ |> wisp.Text
213213+214214+ wisp.internal_server_error()
215215+ |> wisp.set_body(body)
216216+ }
217217+218218+ _ -> {
219219+ let body =
220220+ "Ocorreu um erro ao inserir o usuário no banco de dados"
221221+ |> wisp.Text
222222+223223+ wisp.internal_server_error()
224224+ |> wisp.set_body(body)
225225+ }
226226+ }
227227+ }
228228+ }
229229+ }
230230+ }
231231+ }
232232+ }
233233+}
234234+235235+/// Represents possible errors that can occur during occurrence registration,
236236+/// including invalid UUID formats for applicant, category, or subcategory,
237237+/// and missing authentication cookie.
238238+type RegisterNewOccurrenceError {
239239+ /// The provided applicant ID is not a valid UUID format
240240+ InvalidApplicantUUID(String)
241241+ /// The provided category ID is not a valid UUID format
242242+ InvalidCategoryUUID(String)
243243+ /// The provided subcategory ID is not a valid UUID format
244244+ InvalidSubCategoryUUID(String)
245245+ /// The required user authentication cookie is missing from the request
246246+ MissingCookie
247247+}
+9-4
src/app/routes/signup.gleam
···11+//// Handler for user registration and account creation.
22+////
33+//// It creates new user accounts by validating form data and inserting
44+//// the user information into the database with proper password hashing.
55+////
66+//// Passwords are hashed using Argon2 before storage and all sensitive
77+//// operations are logged for audit purposes.
88+19import app/sql
210import app/web.{type Context}
311import argus
···4452}
45534654/// Inserts a new `user_account` into the database
4747-pub fn handle_form_submission(
4848- request req: wisp.Request,
4949- ctx ctx: Context,
5050-) -> wisp.Response {
5555+pub fn handle_form(request req: wisp.Request, ctx ctx: Context) -> wisp.Response {
5156 use form_data <- wisp.require_form(req)
5257 let form_result =
5358 signup_form()
+148-62
src/app/sql.gleam
···2020 CountActiveBrigadesRow(count: Int)
2121}
22222323-/// Runs the `count_active_brigades` query
2424-/// defined in `./src/app/sql/count_active_brigades.sql`.
2323+/// Counts the number of active brigades in the database.
2524///
2625/// > 🐿️ This function was generated automatically using v4.4.1 of
2726/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
···3433 decode.success(CountActiveBrigadesRow(count:))
3534 }
36353737- "SELECT COUNT(id)
3636+ "-- Counts the number of active brigades in the database.
3737+SELECT COUNT(id)
3838FROM public.brigade
3939WHERE is_active = TRUE;
4040"
···5151///
5252pub type GetBrigadeMembersRow {
5353 GetBrigadeMembersRow(
5454+ id: Uuid,
5455 full_name: String,
5556 role_name: Option(String),
5657 description: Option(String),
5758 )
5859}
59606060-/// Runs the `get_brigade_members` query
6161-/// defined in `./src/app/sql/get_brigade_members.sql`.
6161+/// Find all members of a brigade
6262///
6363/// > 🐿️ This function was generated automatically using v4.4.1 of
6464/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
···6868 arg_1: Uuid,
6969) -> Result(pog.Returned(GetBrigadeMembersRow), pog.QueryError) {
7070 let decoder = {
7171- use full_name <- decode.field(0, decode.string)
7272- use role_name <- decode.field(1, decode.optional(decode.string))
7373- use description <- decode.field(2, decode.optional(decode.string))
7474- decode.success(GetBrigadeMembersRow(full_name:, role_name:, description:))
7171+ use id <- decode.field(0, uuid_decoder())
7272+ use full_name <- decode.field(1, decode.string)
7373+ use role_name <- decode.field(2, decode.optional(decode.string))
7474+ use description <- decode.field(3, decode.optional(decode.string))
7575+ decode.success(GetBrigadeMembersRow(
7676+ id:,
7777+ full_name:,
7878+ role_name:,
7979+ description:,
8080+ ))
7581 }
76827777- "SELECT
8383+ "-- Find all members of a brigade
8484+SELECT
8585+ u.id,
7886 u.full_name,
7987 r.role_name,
8088 r.description
···92100 |> pog.execute(db)
93101}
941029595-/// A row you get from running the `get_fellow_brigade_members` query
9696-/// defined in `./src/app/sql/get_fellow_brigade_members.sql`.
103103+/// A row you get from running the `get_crew_members` query
104104+/// defined in `./src/app/sql/get_crew_members.sql`.
97105///
98106/// > 🐿️ This type definition was generated automatically using v4.4.1 of the
99107/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
100108///
101101-pub type GetFellowBrigadeMembersRow {
102102- GetFellowBrigadeMembersRow(
109109+pub type GetCrewMembersRow {
110110+ GetCrewMembersRow(
111111+ id: Uuid,
103112 full_name: String,
104113 role_name: Option(String),
105114 description: Option(String),
106115 )
107116}
108117109109-/// Runs the `get_fellow_brigade_members` query
110110-/// defined in `./src/app/sql/get_fellow_brigade_members.sql`.
118118+/// Retrieves detailed information about fellow brigade members
119119+/// for a given user, including their names and role details.
111120///
112121/// > 🐿️ This function was generated automatically using v4.4.1 of
113122/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
114123///
115115-pub fn get_fellow_brigade_members(
124124+pub fn get_crew_members(
116125 db: pog.Connection,
117126 arg_1: Uuid,
118118-) -> Result(pog.Returned(GetFellowBrigadeMembersRow), pog.QueryError) {
127127+) -> Result(pog.Returned(GetCrewMembersRow), pog.QueryError) {
119128 let decoder = {
120120- use full_name <- decode.field(0, decode.string)
121121- use role_name <- decode.field(1, decode.optional(decode.string))
122122- use description <- decode.field(2, decode.optional(decode.string))
123123- decode.success(GetFellowBrigadeMembersRow(
124124- full_name:,
125125- role_name:,
126126- description:,
127127- ))
129129+ use id <- decode.field(0, uuid_decoder())
130130+ use full_name <- decode.field(1, decode.string)
131131+ use role_name <- decode.field(2, decode.optional(decode.string))
132132+ use description <- decode.field(3, decode.optional(decode.string))
133133+ decode.success(GetCrewMembersRow(id:, full_name:, role_name:, description:))
128134 }
129135130130- "SELECT
136136+ "-- Retrieves detailed information about fellow brigade members
137137+-- for a given user, including their names and role details.
138138+SELECT
139139+ u.id,
131140 u.full_name,
132141 r.role_name,
133142 r.description
134134-FROM QUERY_FELLOW_BRIGADE_MEMBERS_ID($1) AS fellow_members (id)
143143+FROM QUERY_FELLOW_BRIGADE_MEMBERS_ID($1) AS crew_members (id)
135144INNER JOIN
136145 public.user_account AS u
137137- ON fellow_members.id = u.id
146146+ ON crew_members.id = u.id
138147LEFT JOIN
139148 public.user_role AS r
140149 ON u.role_id = r.id;
···155164 GetLoginTokenRow(id: Uuid, password_hash: String)
156165}
157166158158-/// Runs the `get_login_token` query
159159-/// defined in `./src/app/sql/get_login_token.sql`.
167167+/// Retrieves a user's ID and password hash from their registration
168168+/// number for authentication purposes.
160169///
161170/// > 🐿️ This function was generated automatically using v4.4.1 of
162171/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
···171180 decode.success(GetLoginTokenRow(id:, password_hash:))
172181 }
173182174174- "SELECT
183183+ "-- Retrieves a user's ID and password hash from their registration
184184+-- number for authentication purposes.
185185+SELECT
175186 u.id,
176187 u.password_hash
177188FROM public.user_account AS u
···191202///
192203pub type GetOccurencesByApplicantRow {
193204 GetOccurencesByApplicantRow(
205205+ id: Uuid,
194206 description: Option(String),
195207 category: Option(String),
196208 subcategory: Option(String),
197209 created_at: Option(Timestamp),
198210 resolved_at: Option(Timestamp),
199211 location: List(Float),
200200- reference_point: String,
201201- loss_percentage: Option(Float),
212212+ reference_point: Option(String),
202213 )
203214}
204215205205-/// Runs the `get_occurences_by_applicant` query
206206-/// defined in `./src/app/sql/get_occurences_by_applicant.sql`.
216216+/// Retrieves all occurrences associated with a user,
217217+/// including detailed category information and resolution status.
207218///
208219/// > 🐿️ This function was generated automatically using v4.4.1 of
209220/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
···213224 arg_1: Uuid,
214225) -> Result(pog.Returned(GetOccurencesByApplicantRow), pog.QueryError) {
215226 let decoder = {
216216- use description <- decode.field(0, decode.optional(decode.string))
217217- use category <- decode.field(1, decode.optional(decode.string))
218218- use subcategory <- decode.field(2, decode.optional(decode.string))
219219- use created_at <- decode.field(3, decode.optional(pog.timestamp_decoder()))
220220- use resolved_at <- decode.field(4, decode.optional(pog.timestamp_decoder()))
221221- use location <- decode.field(5, decode.list(decode.float))
222222- use reference_point <- decode.field(6, decode.string)
223223- use loss_percentage <- decode.field(
224224- 7,
225225- decode.optional(pog.numeric_decoder()),
226226- )
227227+ use id <- decode.field(0, uuid_decoder())
228228+ use description <- decode.field(1, decode.optional(decode.string))
229229+ use category <- decode.field(2, decode.optional(decode.string))
230230+ use subcategory <- decode.field(3, decode.optional(decode.string))
231231+ use created_at <- decode.field(4, decode.optional(pog.timestamp_decoder()))
232232+ use resolved_at <- decode.field(5, decode.optional(pog.timestamp_decoder()))
233233+ use location <- decode.field(6, decode.list(decode.float))
234234+ use reference_point <- decode.field(7, decode.optional(decode.string))
227235 decode.success(GetOccurencesByApplicantRow(
236236+ id:,
228237 description:,
229238 category:,
230239 subcategory:,
···232241 resolved_at:,
233242 location:,
234243 reference_point:,
235235- loss_percentage:,
236244 ))
237245 }
238246239239- "SELECT
247247+ "-- Retrieves all occurrences associated with a user,
248248+-- including detailed category information and resolution status.
249249+SELECT
250250+ o.id,
240251 o.description,
241252 oc_cat.category_name AS category,
242253 sub_cat.category_name AS subcategory,
243254 o.created_at,
244255 o.resolved_at,
245256 o.location,
246246- o.reference_point,
247247- o.loss_percentage
257257+ o.reference_point
248258FROM public.query_all_ocurrences_by_user_id($1) AS oc_list (id)
249259INNER JOIN public.occurrence AS o
250260 ON oc_list.id = o.id
···269279 GetUserIdByRegistrationRow(id: Uuid)
270280}
271281272272-/// Runs the `get_user_id_by_registration` query
273273-/// defined in `./src/app/sql/get_user_id_by_registration.sql`.
282282+/// Retrieves a user's ID from their registration number.
274283///
275284/// > 🐿️ This function was generated automatically using v4.4.1 of
276285/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
···284293 decode.success(GetUserIdByRegistrationRow(id:))
285294 }
286295287287- "SELECT u.id
296296+ "-- Retrieves a user's ID from their registration number.
297297+SELECT u.id
288298FROM public.user_account AS u
289299WHERE u.registration = $1;
290300"
···294304 |> pog.execute(db)
295305}
296306297297-/// Runs the `insert_new_user` query
298298-/// defined in `./src/app/sql/insert_new_user.sql`.
307307+/// A row you get from running the `get_user_name` query
308308+/// defined in `./src/app/sql/get_user_name.sql`.
309309+///
310310+/// > 🐿️ This type definition was generated automatically using v4.4.1 of the
311311+/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
312312+///
313313+pub type GetUserNameRow {
314314+ GetUserNameRow(full_name: String)
315315+}
316316+317317+/// Retrieves a user's full name by their user ID.
318318+///
319319+/// > 🐿️ This function was generated automatically using v4.4.1 of
320320+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
321321+///
322322+pub fn get_user_name(
323323+ db: pog.Connection,
324324+ arg_1: Uuid,
325325+) -> Result(pog.Returned(GetUserNameRow), pog.QueryError) {
326326+ let decoder = {
327327+ use full_name <- decode.field(0, decode.string)
328328+ decode.success(GetUserNameRow(full_name:))
329329+ }
330330+331331+ "-- Retrieves a user's full name by their user ID.
332332+SELECT u.full_name
333333+FROM public.user_account AS u
334334+WHERE u.id = $1;
335335+"
336336+ |> pog.query
337337+ |> pog.parameter(pog.text(uuid.to_string(arg_1)))
338338+ |> pog.returning(decoder)
339339+ |> pog.execute(db)
340340+}
341341+342342+/// Inserts a new occurrence into the database
343343+///
344344+/// > 🐿️ This function was generated automatically using v4.4.1 of
345345+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
346346+///
347347+pub fn insert_new_occurence(
348348+ db: pog.Connection,
349349+ arg_1: Uuid,
350350+ arg_2: Uuid,
351351+ arg_3: Uuid,
352352+ arg_4: String,
353353+ arg_5: List(Float),
354354+ arg_6: String,
355355+ arg_7: String,
356356+ arg_8: List(Uuid),
357357+) -> Result(pog.Returned(Nil), pog.QueryError) {
358358+ let decoder = decode.map(decode.dynamic, fn(_) { Nil })
359359+360360+ "-- Inserts a new occurrence into the database
361361+INSERT INTO public.occurrence (
362362+ applicant_id,
363363+ category_id,
364364+ subcategory_id,
365365+ description,
366366+ location,
367367+ reference_point,
368368+ vehicle_code,
369369+ participants_id
370370+) VALUES ($1, $2, $3, $4, $5, $6, $7, $8);
371371+"
372372+ |> pog.query
373373+ |> pog.parameter(pog.text(uuid.to_string(arg_1)))
374374+ |> pog.parameter(pog.text(uuid.to_string(arg_2)))
375375+ |> pog.parameter(pog.text(uuid.to_string(arg_3)))
376376+ |> pog.parameter(pog.text(arg_4))
377377+ |> pog.parameter(pog.array(fn(value) { pog.float(value) }, arg_5))
378378+ |> pog.parameter(pog.text(arg_6))
379379+ |> pog.parameter(pog.text(arg_7))
380380+ |> pog.parameter(
381381+ pog.array(fn(value) { pog.text(uuid.to_string(value)) }, arg_8),
382382+ )
383383+ |> pog.returning(decoder)
384384+ |> pog.execute(db)
385385+}
386386+387387+/// Inserts a new user into the database
299388///
300389/// > 🐿️ This function was generated automatically using v4.4.1 of
301390/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
···310399) -> Result(pog.Returned(Nil), pog.QueryError) {
311400 let decoder = decode.map(decode.dynamic, fn(_) { Nil })
312401313313- "INSERT INTO public.user_account (
402402+ "-- Inserts a new user into the database
403403+INSERT INTO public.user_account (
314404 full_name,
315405 registration,
316406 phone,
317407 email,
318408 password_hash
319409) VALUES (
320320- $1,
321321- $2,
322322- $3,
323323- $4,
324324- $5
410410+ $1, $2, $3, $4, $5
325411)
326412"
327413 |> pog.query
+1
src/app/sql/count_active_brigades.sql
···11+-- Counts the number of active brigades in the database.
12SELECT COUNT(id)
23FROM public.brigade
34WHERE is_active = TRUE;
+2
src/app/sql/get_brigade_members.sql
···11+-- Find all members of a brigade
12SELECT
33+ u.id,
24 u.full_name,
35 r.role_name,
46 r.description
+14
src/app/sql/get_crew_members.sql
···11+-- Retrieves detailed information about fellow brigade members
22+-- for a given user, including their names and role details.
33+SELECT
44+ u.id,
55+ u.full_name,
66+ r.role_name,
77+ r.description
88+FROM QUERY_FELLOW_BRIGADE_MEMBERS_ID($1) AS crew_members (id)
99+INNER JOIN
1010+ public.user_account AS u
1111+ ON crew_members.id = u.id
1212+LEFT JOIN
1313+ public.user_role AS r
1414+ ON u.role_id = r.id;
-11
src/app/sql/get_fellow_brigade_members.sql
···11-SELECT
22- u.full_name,
33- r.role_name,
44- r.description
55-FROM QUERY_FELLOW_BRIGADE_MEMBERS_ID($1) AS fellow_members (id)
66-INNER JOIN
77- public.user_account AS u
88- ON fellow_members.id = u.id
99-LEFT JOIN
1010- public.user_role AS r
1111- ON u.role_id = r.id;
+2
src/app/sql/get_login_token.sql
···11+-- Retrieves a user's ID and password hash from their registration
22+-- number for authentication purposes.
13SELECT
24 u.id,
35 u.password_hash
+4-2
src/app/sql/get_occurences_by_applicant.sql
···11+-- Retrieves all occurrences associated with a user,
22+-- including detailed category information and resolution status.
13SELECT
44+ o.id,
25 o.description,
36 oc_cat.category_name AS category,
47 sub_cat.category_name AS subcategory,
58 o.created_at,
69 o.resolved_at,
710 o.location,
88- o.reference_point,
99- o.loss_percentage
1111+ o.reference_point
1012FROM public.query_all_ocurrences_by_user_id($1) AS oc_list (id)
1113INNER JOIN public.occurrence AS o
1214 ON oc_list.id = o.id
+1
src/app/sql/get_user_id_by_registration.sql
···11+-- Retrieves a user's ID from their registration number.
12SELECT u.id
23FROM public.user_account AS u
34WHERE u.registration = $1;
+4
src/app/sql/get_user_name.sql
···11+-- Retrieves a user's full name by their user ID.
22+SELECT u.full_name
33+FROM public.user_account AS u
44+WHERE u.id = $1;
+11
src/app/sql/insert_new_occurence.sql
···11+-- Inserts a new occurrence into the database
22+INSERT INTO public.occurrence (
33+ applicant_id,
44+ category_id,
55+ subcategory_id,
66+ description,
77+ location,
88+ reference_point,
99+ vehicle_code,
1010+ participants_id
1111+) VALUES ($1, $2, $3, $4, $5, $6, $7, $8);
+2-5
src/app/sql/insert_new_user.sql
···11+-- Inserts a new user into the database
12INSERT INTO public.user_account (
23 full_name,
34 registration,
···56 email,
67 password_hash
78) VALUES (
88- $1,
99- $2,
1010- $3,
1111- $4,
1212- $5
99+ $1, $2, $3, $4, $5
1310)
+21
src/app/web.gleam
···11+//// Web application context and middleware configuration.
22+////
33+//// This module defines the core web infrastructure including:
44+//// - The `Context` type that holds application dependencies for request handlers
55+//// - Middleware pipeline for request processing
66+//// - CORS configuration for cross-origin requests
77+//// - Logger configuration for application logging
88+////
99+//// ## Middleware Pipeline
1010+//// The middleware function applies the following processing to each request:
1111+//// - HTTP method overriding (for REST clients)
1212+//// - Request logging
1313+//// - Crash recovery and error handling
1414+//// - HEAD request normalization
1515+//// - CORS headers
1616+//// - Static file serving from `/static` path
1717+////
1818+//// ## CORS Configuration
1919+//// Currently configured to allow requests from `http://localhost:5173`
2020+//// with GET and POST methods enabled.
2121+122import cors_builder as cors
223import gleam/http
324import glight