(refactor) removed username from logic
This commit is contained in:
parent
0977d47ec2
commit
e4cfc5eee5
15 changed files with 233 additions and 122 deletions
28
.sqlx/query-36a82976eed28484e6fb3becd13fa8f3e04e8ed027114b175a1b07e872b2732e.json
generated
Normal file
28
.sqlx/query-36a82976eed28484e6fb3becd13fa8f3e04e8ed027114b175a1b07e872b2732e.json
generated
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT id, password_hash\n FROM users\n WHERE bank_user_id = $1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Int4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "password_hash",
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "36a82976eed28484e6fb3becd13fa8f3e04e8ed027114b175a1b07e872b2732e"
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT id, password_hash, bank_user_id\n FROM users\n WHERE username = $1\n ",
|
"query": "\n INSERT INTO users (bank_user_id, password_hash)\n VALUES ($1, $2)\n RETURNING id, bank_user_id\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
|
@ -10,25 +10,20 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 1,
|
"ordinal": 1,
|
||||||
"name": "password_hash",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 2,
|
|
||||||
"name": "bank_user_id",
|
"name": "bank_user_id",
|
||||||
"type_info": "Varchar"
|
"type_info": "Varchar"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Left": [
|
"Left": [
|
||||||
|
"Varchar",
|
||||||
"Text"
|
"Text"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
false,
|
|
||||||
false,
|
false,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "5f7d837ff17893ba5aa8d004e2ba89d8a8da04b967f08bb1b10542c934f7c5de"
|
"hash": "802dc1370bbb3bb38f7e83cdf7008aa6a44f2cf7a630390c66fe95e56fcf944f"
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT id, username, bank_user_id\n FROM users\n WHERE id = $1\n ",
|
"query": "\n SELECT id, bank_user_id\n FROM users\n WHERE id = $1\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
|
@ -10,11 +10,6 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 1,
|
"ordinal": 1,
|
||||||
"name": "username",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 2,
|
|
||||||
"name": "bank_user_id",
|
"name": "bank_user_id",
|
||||||
"type_info": "Varchar"
|
"type_info": "Varchar"
|
||||||
}
|
}
|
||||||
|
|
@ -25,10 +20,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
false,
|
|
||||||
false,
|
false,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "e002906973088f3105c0a1f4a572cf084534c0b1be339f371f256c8149358440"
|
"hash": "9fe0c11697bc27b1f32a5cbe3bfdcfc17d275a5c088eb72cc4a8d7a3d7a86f08"
|
||||||
}
|
}
|
||||||
29
.sqlx/query-abbec0055ff4abbb562f9924626679db8c42489cbcf167e8193d2674143313b3.json
generated
Normal file
29
.sqlx/query-abbec0055ff4abbb562f9924626679db8c42489cbcf167e8193d2674143313b3.json
generated
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT account_id, nickname\n FROM accounts\n WHERE user_id = $1 AND bank_code = $2\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "account_id",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "nickname",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text",
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "abbec0055ff4abbb562f9924626679db8c42489cbcf167e8193d2674143313b3"
|
||||||
|
}
|
||||||
23
.sqlx/query-d0f42e2b8bb9d1bb3ab65fcd208c80a1956f883407d05bd680760b4cc8885968.json
generated
Normal file
23
.sqlx/query-d0f42e2b8bb9d1bb3ab65fcd208c80a1956f883407d05bd680760b4cc8885968.json
generated
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n INSERT INTO accounts \n (account_id, user_id, bank_code, status, currency, account_type, \n account_sub_type, nickname, description, opening_date)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ON CONFLICT (account_id, bank_code)\n DO UPDATE SET updated_at = NOW()\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Varchar",
|
||||||
|
"Varchar",
|
||||||
|
"Varchar",
|
||||||
|
"Varchar",
|
||||||
|
"Varchar",
|
||||||
|
"Varchar",
|
||||||
|
"Varchar",
|
||||||
|
"Varchar",
|
||||||
|
"Text",
|
||||||
|
"Date"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "d0f42e2b8bb9d1bb3ab65fcd208c80a1956f883407d05bd680760b4cc8885968"
|
||||||
|
}
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "PostgreSQL",
|
|
||||||
"query": "\n INSERT INTO users (username, password_hash, bank_user_id)\n VALUES ($1, $2, $3)\n RETURNING id, username, bank_user_id\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"ordinal": 0,
|
|
||||||
"name": "id",
|
|
||||||
"type_info": "Int4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 1,
|
|
||||||
"name": "username",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 2,
|
|
||||||
"name": "bank_user_id",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Varchar",
|
|
||||||
"Text",
|
|
||||||
"Varchar"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "dc09058128a7c72fa340f74b522fa111420a69e49170f6af47503b73fccbd9db"
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
-- migrations/XXXXXX_create_users_table.sql
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
username VARCHAR(50) UNIQUE NOT NULL,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
bank_user_id VARCHAR(50) NOT NULL, -- e.g., "team275-1"
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
|
|
||||||
CONSTRAINT username_length CHECK (char_length(username) >= 3)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_bank_user_id ON users(bank_user_id);
|
|
||||||
12
migrations/20251107120039_drop_and_recreate_users_table.sql
Normal file
12
migrations/20251107120039_drop_and_recreate_users_table.sql
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
-- Drop old table
|
||||||
|
DROP TABLE IF EXISTS users CASCADE;
|
||||||
|
|
||||||
|
-- Create simplified users table
|
||||||
|
CREATE TABLE users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
bank_user_id VARCHAR(50) UNIQUE NOT NULL, -- e.g., "team275-6"
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_bank_user_id ON users(bank_user_id);
|
||||||
|
|
@ -6,3 +6,4 @@ pub mod middleware;
|
||||||
pub mod password;
|
pub mod password;
|
||||||
|
|
||||||
pub use jwt::Claims;
|
pub use jwt::Claims;
|
||||||
|
pub use middleware::auth_middleware;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
// src/auth/handlers.rs
|
// src/auth/handlers.rs
|
||||||
// Authentication HTTP handlers
|
// Authentication HTTP handlers
|
||||||
|
// User registers with bank_user_number (1-10) + password
|
||||||
|
// Gets JWT for accessing Multiberry endpoints
|
||||||
|
// Real credentials: bank_user_id + password
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Extension, State},
|
extract::State,
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
Json,
|
Json,
|
||||||
|
|
@ -11,24 +14,23 @@ use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::{state::AppState, db};
|
use crate::{state::AppState, db};
|
||||||
use super::{jwt::{generate_token, Claims}, password::{hash_password, verify_password}};
|
use super::{jwt::generate_token, password::{hash_password, verify_password}};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct RegisterRequest {
|
pub struct RegisterRequest {
|
||||||
pub username: String,
|
pub bank_user_number: u8, // 1-10: User picks their slot (e.g., 6 → team275-6)
|
||||||
pub password: String,
|
pub password: String, // User-created password
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct LoginRequest {
|
pub struct LoginRequest {
|
||||||
pub username: String,
|
pub bank_user_id: String, // e.g., "team275-6"
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct AuthResponse {
|
pub struct AuthResponse {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
pub username: String,
|
|
||||||
pub bank_user_id: String,
|
pub bank_user_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,6 +38,7 @@ pub async fn register_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(payload): Json<RegisterRequest>,
|
Json(payload): Json<RegisterRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
|
// Validate password length
|
||||||
if payload.password.len() < 3 {
|
if payload.password.len() < 3 {
|
||||||
return Err((
|
return Err((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
|
|
@ -43,23 +46,34 @@ pub async fn register_handler(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate bank_user_number (1-10)
|
||||||
|
if payload.bank_user_number < 1 || payload.bank_user_number > 10 {
|
||||||
|
return Err((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(json!({ "error": "bank_user_number must be between 1 and 10" }))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
let password_hash = hash_password(&payload.password)
|
let password_hash = hash_password(&payload.password)
|
||||||
.map_err(|_| (
|
.map_err(|_| (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(json!({ "error": "Failed to hash password" }))
|
Json(json!({ "error": "Failed to hash password" }))
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
// Generate bank_user_id based on username (you can make this more sophisticated)
|
// Build bank_user_id from environment + number
|
||||||
let bank_user_id = format!("team275-{}", payload.username);
|
let bank_user_id = format!("{}-{}", state.bank_team_id, payload.bank_user_number);
|
||||||
|
|
||||||
let user = db::users::create_user(&state.db_pool, &payload.username, &password_hash, &bank_user_id)
|
// Create user in database
|
||||||
|
let user = db::users::create_user(&state.db_pool, &bank_user_id, &password_hash)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (
|
.map_err(|e| (
|
||||||
StatusCode::CONFLICT,
|
StatusCode::CONFLICT,
|
||||||
Json(json!({ "error": format!("Username already exists or database error: {}", e) }))
|
Json(json!({ "error": format!("User already exists or database error: {}", e) }))
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
let token = generate_token(user.id, &user.username, &user.bank_user_id)
|
// Generate JWT token
|
||||||
|
let token = generate_token(user.id, &user.bank_user_id)
|
||||||
.map_err(|_| (
|
.map_err(|_| (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(json!({ "error": "Failed to generate token" }))
|
Json(json!({ "error": "Failed to generate token" }))
|
||||||
|
|
@ -69,7 +83,6 @@ pub async fn register_handler(
|
||||||
StatusCode::CREATED,
|
StatusCode::CREATED,
|
||||||
Json(AuthResponse {
|
Json(AuthResponse {
|
||||||
token,
|
token,
|
||||||
username: user.username,
|
|
||||||
bank_user_id: user.bank_user_id,
|
bank_user_id: user.bank_user_id,
|
||||||
})
|
})
|
||||||
))
|
))
|
||||||
|
|
@ -79,7 +92,8 @@ pub async fn login_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(payload): Json<LoginRequest>,
|
Json(payload): Json<LoginRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let user_data = db::users::get_user_by_username(&state.db_pool, &payload.username)
|
// Look up user by bank_user_id
|
||||||
|
let user_data = db::users::get_user_by_bank_user_id(&state.db_pool, &payload.bank_user_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| (
|
.map_err(|_| (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
|
@ -87,11 +101,12 @@ pub async fn login_handler(
|
||||||
))?
|
))?
|
||||||
.ok_or_else(|| (
|
.ok_or_else(|| (
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
Json(json!({ "error": "Invalid username or password" }))
|
Json(json!({ "error": "Invalid bank_user_id or password" }))
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
let (user_id, password_hash, bank_user_id) = user_data;
|
let (user_id, password_hash) = user_data;
|
||||||
|
|
||||||
|
// Verify password
|
||||||
let is_valid = verify_password(&payload.password, &password_hash)
|
let is_valid = verify_password(&payload.password, &password_hash)
|
||||||
.map_err(|_| (
|
.map_err(|_| (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
|
@ -101,11 +116,12 @@ pub async fn login_handler(
|
||||||
if !is_valid {
|
if !is_valid {
|
||||||
return Err((
|
return Err((
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
Json(json!({ "error": "Invalid username or password" }))
|
Json(json!({ "error": "Invalid bank_user_id or password" }))
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = generate_token(user_id, &payload.username, &bank_user_id)
|
// Generate JWT token
|
||||||
|
let token = generate_token(user_id, &payload.bank_user_id)
|
||||||
.map_err(|_| (
|
.map_err(|_| (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(json!({ "error": "Failed to generate token" }))
|
Json(json!({ "error": "Failed to generate token" }))
|
||||||
|
|
@ -115,20 +131,18 @@ pub async fn login_handler(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(AuthResponse {
|
Json(AuthResponse {
|
||||||
token,
|
token,
|
||||||
username: payload.username,
|
bank_user_id: payload.bank_user_id,
|
||||||
bank_user_id,
|
|
||||||
})
|
})
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn me_handler(
|
pub async fn me_handler(
|
||||||
Extension(claims): Extension<Claims>,
|
axum::extract::Extension(claims): axum::extract::Extension<super::jwt::Claims>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
(
|
(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(json!({
|
Json(json!({
|
||||||
"user_id": claims.sub,
|
"user_id": claims.sub,
|
||||||
"username": claims.username,
|
|
||||||
"bank_user_id": claims.bank_user_id,
|
"bank_user_id": claims.bank_user_id,
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// src/auth/jwt.rs
|
// src/auth/jwt.rs
|
||||||
// JWT token generation and validation
|
// JWT token generation and validation
|
||||||
|
// Token contains bank_user_id so backend knows which Multiberry user is calling
|
||||||
|
|
||||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -8,18 +9,16 @@ use std::env;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Claims {
|
pub struct Claims {
|
||||||
pub sub: i32, // user_id
|
pub sub: i32, // Internal user_id from database
|
||||||
pub username: String,
|
pub bank_user_id: String, // e.g., "team275-6" - identifies which Multiberry user
|
||||||
pub bank_user_id: String,
|
pub exp: i64, // Expiration timestamp
|
||||||
pub exp: i64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_token(user_id: i32, username: &str, bank_user_id: &str) -> Result<String, jsonwebtoken::errors::Error> {
|
pub fn generate_token(user_id: i32, bank_user_id: &str) -> Result<String, jsonwebtoken::errors::Error> {
|
||||||
let secret = env::var("JWT_SECRET").unwrap_or_else(|_| "super_secret_key_change_in_production".to_string());
|
let secret = env::var("JWT_SECRET").unwrap_or_else(|_| "super_secret_key_change_in_production".to_string());
|
||||||
|
|
||||||
let claims = Claims {
|
let claims = Claims {
|
||||||
sub: user_id,
|
sub: user_id,
|
||||||
username: username.to_string(),
|
|
||||||
bank_user_id: bank_user_id.to_string(),
|
bank_user_id: bank_user_id.to_string(),
|
||||||
exp: (Utc::now() + Duration::days(7)).timestamp(),
|
exp: (Utc::now() + Duration::days(7)).timestamp(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
// src/db/accounts.rs
|
||||||
|
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use chrono::{DateTime, Utc, NaiveDate};
|
||||||
|
|
||||||
|
pub async fn store_account(
|
||||||
|
pool: &PgPool,
|
||||||
|
account_id: &str,
|
||||||
|
user_id: &str, // team275-6
|
||||||
|
bank_code: &str, // vbank
|
||||||
|
status: &str,
|
||||||
|
currency: &str,
|
||||||
|
account_type: &str,
|
||||||
|
account_sub_type: Option<&str>,
|
||||||
|
nickname: &str,
|
||||||
|
description: Option<&str>,
|
||||||
|
opening_date: Option<NaiveDate>,
|
||||||
|
) -> Result<(), sqlx::Error> {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO accounts
|
||||||
|
(account_id, user_id, bank_code, status, currency, account_type,
|
||||||
|
account_sub_type, nickname, description, opening_date)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
|
ON CONFLICT (account_id, bank_code)
|
||||||
|
DO UPDATE SET updated_at = NOW()
|
||||||
|
"#,
|
||||||
|
account_id,
|
||||||
|
user_id,
|
||||||
|
bank_code,
|
||||||
|
status,
|
||||||
|
currency,
|
||||||
|
account_type,
|
||||||
|
account_sub_type,
|
||||||
|
nickname,
|
||||||
|
description,
|
||||||
|
opening_date
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_accounts_for_user(
|
||||||
|
pool: &PgPool,
|
||||||
|
user_id: &str,
|
||||||
|
bank_code: &str,
|
||||||
|
) -> Result<Vec<(String, String)>, sqlx::Error> { // Returns (account_id, nickname)
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT account_id, nickname
|
||||||
|
FROM accounts
|
||||||
|
WHERE user_id = $1 AND bank_code = $2
|
||||||
|
"#,
|
||||||
|
user_id,
|
||||||
|
bank_code
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.map(|rows| rows.into_iter().map(|r| (r.account_id, r.nickname)).collect())
|
||||||
|
}
|
||||||
|
|
@ -5,46 +5,43 @@ use sqlx::PgPool;
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub username: String,
|
pub bank_user_id: String,
|
||||||
pub bank_user_id: String, // Keep this - it's team275-X
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_user(
|
pub async fn create_user(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
username: &str,
|
|
||||||
password_hash: &str,
|
|
||||||
bank_user_id: &str,
|
bank_user_id: &str,
|
||||||
|
password_hash: &str,
|
||||||
) -> Result<User, sqlx::Error> {
|
) -> Result<User, sqlx::Error> {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
User,
|
User,
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO users (username, password_hash, bank_user_id)
|
INSERT INTO users (bank_user_id, password_hash)
|
||||||
VALUES ($1, $2, $3)
|
VALUES ($1, $2)
|
||||||
RETURNING id, username, bank_user_id
|
RETURNING id, bank_user_id
|
||||||
"#,
|
"#,
|
||||||
username,
|
bank_user_id,
|
||||||
password_hash,
|
password_hash
|
||||||
bank_user_id
|
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_by_username(
|
pub async fn get_user_by_bank_user_id(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
username: &str,
|
bank_user_id: &str,
|
||||||
) -> Result<Option<(i32, String, String)>, sqlx::Error> {
|
) -> Result<Option<(i32, String)>, sqlx::Error> {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
SELECT id, password_hash, bank_user_id
|
SELECT id, password_hash
|
||||||
FROM users
|
FROM users
|
||||||
WHERE username = $1
|
WHERE bank_user_id = $1
|
||||||
"#,
|
"#,
|
||||||
username
|
bank_user_id
|
||||||
)
|
)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await
|
.await
|
||||||
.map(|row| row.map(|r| (r.id, r.password_hash, r.bank_user_id)))
|
.map(|row| row.map(|r| (r.id, r.password_hash)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_by_id(
|
pub async fn get_user_by_id(
|
||||||
|
|
@ -54,7 +51,7 @@ pub async fn get_user_by_id(
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
User,
|
User,
|
||||||
r#"
|
r#"
|
||||||
SELECT id, username, bank_user_id
|
SELECT id, bank_user_id
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
"#,
|
"#,
|
||||||
|
|
|
||||||
16
src/route.rs
16
src/route.rs
|
|
@ -2,29 +2,29 @@
|
||||||
|
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
|
|
||||||
use axum::{routing::{get, post}, Router};
|
use axum::{middleware, routing::{get, post}, Router};
|
||||||
use crate::state::AppState;
|
use crate::{state::AppState, auth};
|
||||||
|
|
||||||
pub fn router(app_state: AppState) -> Router {
|
pub fn router(app_state: AppState) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
// Health check
|
// Public routes (no auth required)
|
||||||
.route("/api/health", get(handlers::health_handler))
|
.route("/api/health", get(handlers::health_handler))
|
||||||
|
.route("/api/auth/register", post(auth::handlers::register_handler))
|
||||||
|
.route("/api/auth/login", post(auth::handlers::login_handler))
|
||||||
|
|
||||||
// Consent management
|
// Protected routes (auth required)
|
||||||
|
.route("/api/auth/me", get(auth::handlers::me_handler))
|
||||||
.route("/api/consent/:bank/:user_id",
|
.route("/api/consent/:bank/:user_id",
|
||||||
post(handlers::create_consent_handler)
|
post(handlers::create_consent_handler)
|
||||||
.get(handlers::get_consent_handler)
|
.get(handlers::get_consent_handler)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Account access
|
|
||||||
.route("/api/accounts/:bank/:user_id",
|
.route("/api/accounts/:bank/:user_id",
|
||||||
get(handlers::get_accounts_handler)
|
get(handlers::get_accounts_handler)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Transaction access
|
|
||||||
.route("/api/transactions/:bank/:user_id/:account_id",
|
.route("/api/transactions/:bank/:user_id/:account_id",
|
||||||
get(handlers::get_transactions_handler)
|
get(handlers::get_transactions_handler)
|
||||||
)
|
)
|
||||||
|
.layer(middleware::from_fn(auth::auth_middleware))
|
||||||
|
|
||||||
.with_state(app_state)
|
.with_state(app_state)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
src/state.rs
13
src/state.rs
|
|
@ -3,12 +3,14 @@
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::api::BankingClients;
|
use crate::api::BankingClients;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
/// Общее состояние приложения, доступное во всех handlers
|
/// Global application state, accessible in all handlers
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub db_pool: PgPool,
|
pub db_pool: PgPool,
|
||||||
pub banking_clients: BankingClients,
|
pub banking_clients: BankingClients,
|
||||||
|
pub bank_team_id: String, // e.g., "team275" from VBANK_CLIENT_ID env var
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
|
|
@ -16,13 +18,18 @@ impl AppState {
|
||||||
let db_pool = db::init_pool().await;
|
let db_pool = db::init_pool().await;
|
||||||
println!("✅ Database connection pool created successfully.");
|
println!("✅ Database connection pool created successfully.");
|
||||||
|
|
||||||
// FIX: Add .await and handle the potential error
|
let banking_clients = BankingClients::new()
|
||||||
let banking_clients = BankingClients::new().await.expect("Failed to initialize banking clients");
|
.await
|
||||||
|
.expect("Failed to initialize banking clients");
|
||||||
println!("✅ Banking API clients initialized.");
|
println!("✅ Banking API clients initialized.");
|
||||||
|
|
||||||
|
let bank_team_id = env::var("VBANK_CLIENT_ID")
|
||||||
|
.expect("VBANK_CLIENT_ID environment variable must be set");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
db_pool,
|
db_pool,
|
||||||
banking_clients,
|
banking_clients,
|
||||||
|
bank_team_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue