From c4207b04e6de705ad590964a4625b079dc372b2d Mon Sep 17 00:00:00 2001 From: NathanJ60 Date: Sat, 13 Dec 2025 12:51:50 +0100 Subject: [PATCH] feat: store IP address on user applications (#436) Closes #294 --- .../handlers/auth/create_user_application.rs | 12 ++++++++++-- ...7d3ec99a052e7f28920a8802c096f3e687c6a.json} | 15 +++++++++++---- .../migrations/20250312215600_initdb.sql | 1 + backend/storage/src/models/user_application.rs | 4 +++- .../user_application_repository.rs | 18 +++++++++++------- 5 files changed, 36 insertions(+), 14 deletions(-) rename backend/storage/.sqlx/{query-1f6d24cb5529ad67db890fedb4e722bd7395bc3beacfd770acaac11ea389bb5f.json => query-c26a5cd31c8ba876b555639c3e77d3ec99a052e7f28920a8802c096f3e687c6a.json} (68%) diff --git a/backend/api/src/handlers/auth/create_user_application.rs b/backend/api/src/handlers/auth/create_user_application.rs index 769fb290..310c85d4 100644 --- a/backend/api/src/handlers/auth/create_user_application.rs +++ b/backend/api/src/handlers/auth/create_user_application.rs @@ -1,12 +1,13 @@ use crate::Arcadia; use actix_web::{ web::{Data, Json}, - HttpResponse, + HttpRequest, HttpResponse, }; use arcadia_common::error::Result; use arcadia_storage::{ models::user_application::{UserApplication, UserCreatedUserApplication}, redis::RedisPoolInterface, + sqlx::types::ipnetwork::IpNetwork, }; #[utoipa::path( @@ -20,11 +21,18 @@ use arcadia_storage::{ )] pub async fn exec( arc: Data>, + req: HttpRequest, application: Json, ) -> Result { + let client_ip = req + .connection_info() + .realip_remote_addr() + .and_then(|ip| ip.parse::().ok()) + .unwrap(); + let created_application = arc .pool - .create_user_application(&application.into_inner()) + .create_user_application(&application.into_inner(), client_ip) .await?; Ok(HttpResponse::Created().json(created_application)) diff --git a/backend/storage/.sqlx/query-1f6d24cb5529ad67db890fedb4e722bd7395bc3beacfd770acaac11ea389bb5f.json b/backend/storage/.sqlx/query-c26a5cd31c8ba876b555639c3e77d3ec99a052e7f28920a8802c096f3e687c6a.json similarity index 68% rename from backend/storage/.sqlx/query-1f6d24cb5529ad67db890fedb4e722bd7395bc3beacfd770acaac11ea389bb5f.json rename to backend/storage/.sqlx/query-c26a5cd31c8ba876b555639c3e77d3ec99a052e7f28920a8802c096f3e687c6a.json index 19221155..de9b0e2a 100644 --- a/backend/storage/.sqlx/query-1f6d24cb5529ad67db890fedb4e722bd7395bc3beacfd770acaac11ea389bb5f.json +++ b/backend/storage/.sqlx/query-c26a5cd31c8ba876b555639c3e77d3ec99a052e7f28920a8802c096f3e687c6a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO user_applications (body, referral, email, staff_note, status)\n VALUES ($1, $2, $3, '', 'pending')\n RETURNING id, created_at, body, email, referral, staff_note,\n status as \"status: UserApplicationStatus\"\n ", + "query": "\n INSERT INTO user_applications (body, referral, email, applied_from_ip, staff_note, status)\n VALUES ($1, $2, $3, $4, '', 'pending')\n RETURNING id, created_at, body, email, referral,\n applied_from_ip as \"applied_from_ip: IpNetwork\",\n staff_note, status as \"status: UserApplicationStatus\"\n ", "describe": { "columns": [ { @@ -30,11 +30,16 @@ }, { "ordinal": 5, + "name": "applied_from_ip: IpNetwork", + "type_info": "Inet" + }, + { + "ordinal": 6, "name": "staff_note", "type_info": "Text" }, { - "ordinal": 6, + "ordinal": 7, "name": "status: UserApplicationStatus", "type_info": { "Custom": { @@ -54,7 +59,8 @@ "Left": [ "Text", "Text", - "Text" + "Text", + "Inet" ] }, "nullable": [ @@ -64,8 +70,9 @@ false, false, false, + false, false ] }, - "hash": "1f6d24cb5529ad67db890fedb4e722bd7395bc3beacfd770acaac11ea389bb5f" + "hash": "c26a5cd31c8ba876b555639c3e77d3ec99a052e7f28920a8802c096f3e687c6a" } diff --git a/backend/storage/migrations/20250312215600_initdb.sql b/backend/storage/migrations/20250312215600_initdb.sql index 940c8b30..76ae48b4 100644 --- a/backend/storage/migrations/20250312215600_initdb.sql +++ b/backend/storage/migrations/20250312215600_initdb.sql @@ -98,6 +98,7 @@ CREATE TABLE user_applications ( body TEXT NOT NULL, referral TEXT NOT NULL, email TEXT NOT NULL, + applied_from_ip INET NOT NULL, staff_note TEXT NOT NULL DEFAULT '', status user_application_status_enum NOT NULL DEFAULT 'pending' ); diff --git a/backend/storage/src/models/user_application.rs b/backend/storage/src/models/user_application.rs index bb0b0b32..61bd4873 100644 --- a/backend/storage/src/models/user_application.rs +++ b/backend/storage/src/models/user_application.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use sqlx::prelude::FromRow; +use sqlx::{prelude::FromRow, types::ipnetwork::IpNetwork}; use strum::Display; use utoipa::ToSchema; @@ -23,6 +23,8 @@ pub struct UserApplication { pub body: String, pub email: String, pub referral: String, + #[schema(value_type = String)] + pub applied_from_ip: IpNetwork, pub staff_note: String, pub status: UserApplicationStatus, } diff --git a/backend/storage/src/repositories/user_application_repository.rs b/backend/storage/src/repositories/user_application_repository.rs index 09236bae..98455bb2 100644 --- a/backend/storage/src/repositories/user_application_repository.rs +++ b/backend/storage/src/repositories/user_application_repository.rs @@ -5,24 +5,28 @@ use crate::{ }, }; use arcadia_common::error::{Error, Result}; +use sqlx::types::ipnetwork::IpNetwork; use std::borrow::Borrow; impl ConnectionPool { pub async fn create_user_application( &self, application: &UserCreatedUserApplication, + from_ip: IpNetwork, ) -> Result { let created_application = sqlx::query_as!( UserApplication, r#" - INSERT INTO user_applications (body, referral, email, staff_note, status) - VALUES ($1, $2, $3, '', 'pending') - RETURNING id, created_at, body, email, referral, staff_note, - status as "status: UserApplicationStatus" + INSERT INTO user_applications (body, referral, email, applied_from_ip, staff_note, status) + VALUES ($1, $2, $3, $4, '', 'pending') + RETURNING id, created_at, body, email, referral, + applied_from_ip as "applied_from_ip: IpNetwork", + staff_note, status as "status: UserApplicationStatus" "#, application.body, application.referral, - application.email + application.email, + from_ip ) .fetch_one(self.borrow()) .await @@ -39,7 +43,7 @@ impl ConnectionPool { ) -> Result> { let query = format!( r#" - SELECT id, created_at, body, email, referral, staff_note, + SELECT id, created_at, body, email, referral, applied_from_ip, staff_note, status::user_application_status_enum as status FROM user_applications ua WHERE $1 IS NULL OR ua.status = $1::user_application_status_enum @@ -69,7 +73,7 @@ impl ConnectionPool { UPDATE user_applications SET status = $2::user_application_status_enum WHERE id = $1 - RETURNING id, created_at, body, email, referral, staff_note, + RETURNING id, created_at, body, email, referral, applied_from_ip, staff_note, status::user_application_status_enum as status "#, )