From d1b5223f50de6438754ee52c658c1b0f5c0d1301 Mon Sep 17 00:00:00 2001 From: PeaTree-Maker Date: Sun, 18 Jan 2026 12:01:52 +0000 Subject: [PATCH 1/3] Add currency preference feature --- backend/package-lock.json | 6 ++ backend/src/db/mod.rs | 6 ++ backend/src/handlers/auth.rs | 59 +++++++++++++++++- backend/src/lib.rs | 1 + backend/src/openapi/mod.rs | 4 +- frontend/package-lock.json | 16 ++++- frontend/src/api/client.ts | 13 ++-- frontend/src/components/BudgetSection.tsx | 6 +- frontend/src/components/FixedExpenses.tsx | 8 ++- frontend/src/components/IncomeSection.tsx | 4 +- frontend/src/components/ItemsSection.tsx | 4 +- .../src/components/ProjectedSavingsCard.tsx | 4 +- .../src/components/RetirementSavingsCard.tsx | 4 +- frontend/src/components/SavingsCard.tsx | 8 ++- frontend/src/components/Summary.tsx | 4 +- frontend/src/context/AuthContext.tsx | 18 ++++-- frontend/src/hooks/useCurrency.ts | 11 ++++ frontend/src/lib/currency.ts | 38 ++++++++++++ frontend/src/pages/Settings.tsx | 62 ++++++++++++++++++- package-lock.json | 6 ++ 20 files changed, 254 insertions(+), 28 deletions(-) create mode 100644 backend/package-lock.json create mode 100644 frontend/src/hooks/useCurrency.ts create mode 100644 frontend/src/lib/currency.ts create mode 100644 package-lock.json diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..dfb18f1 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "backend", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/backend/src/db/mod.rs b/backend/src/db/mod.rs index f892d71..84ddbcb 100644 --- a/backend/src/db/mod.rs +++ b/backend/src/db/mod.rs @@ -39,6 +39,12 @@ pub async fn run_migrations(pool: &SqlitePool) -> Result<(), sqlx::Error> { .await .ok(); + + sqlx::query("ALTER TABLE users ADD COLUMN currency TEXT NOT NULL DEFAULT 'USD'") + .execute(pool) + .await + .ok(); + sqlx::query("UPDATE users SET retirement_savings = roth_ira WHERE retirement_savings = 0 AND roth_ira IS NOT NULL AND roth_ira > 0") .execute(pool) .await diff --git a/backend/src/handlers/auth.rs b/backend/src/handlers/auth.rs index a624013..31f8b9e 100644 --- a/backend/src/handlers/auth.rs +++ b/backend/src/handlers/auth.rs @@ -27,6 +27,7 @@ pub struct AuthRequest { pub struct AuthResponse { pub id: i64, pub username: String, + pub currency: String, } #[utoipa::path( @@ -65,6 +66,7 @@ pub async fn register( Ok(Json(AuthResponse { id: result, username: payload.username, + currency: "USD".to_string(), })) } @@ -87,8 +89,8 @@ pub async fn login( Json(payload): Json, ) -> Result { payload.validate()?; - let user: (i64, String, String) = - sqlx::query_as("SELECT id, username, password_hash FROM users WHERE username = ?") + let user: (i64, String, String, String) = + sqlx::query_as("SELECT id, username, password_hash, currency FROM users WHERE username = ?") .bind(&payload.username) .fetch_optional(&pool) .await? @@ -128,6 +130,7 @@ pub async fn login( Json(AuthResponse { id: user.0, username: user.1, + currency: user.3, }), )) } @@ -169,7 +172,7 @@ pub async fn me( State(pool): State, axum::Extension(claims): axum::Extension, ) -> Result, PaymeError> { - let user: (i64, String) = sqlx::query_as("SELECT id, username FROM users WHERE id = ?") + let user: (i64, String, String) = sqlx::query_as("SELECT id, username, currency FROM users WHERE id = ?") .bind(claims.sub) .fetch_optional(&pool) .await? @@ -178,6 +181,7 @@ pub async fn me( Ok(Json(AuthResponse { id: user.0, username: user.1, + currency: user.2, // ADDED })) } @@ -240,9 +244,16 @@ pub async fn change_username( .execute(&pool) .await?; + // ADDED: Fetch currency + let currency: (String,) = sqlx::query_as("SELECT currency FROM users WHERE id = ?") + .bind(claims.sub) + .fetch_one(&pool) + .await?; + Ok(Json(AuthResponse { id: claims.sub, username: payload.new_username, + currency: currency.0, // ADDED })) } @@ -359,3 +370,45 @@ pub async fn clear_all_data( Json(serde_json::json!({"message": "All data cleared"})), )) } +#[derive(Deserialize, ToSchema, Validate)] +pub struct ChangeCurrencyRequest { + #[validate(length(min = 3, max = 3))] + pub currency: String, +} + +#[utoipa::path( + put, + path = "/api/auth/change-currency", + request_body = ChangeCurrencyRequest, + responses( + (status = 200, description = "Currency changed successfully", body = AuthResponse), + (status = 500, description = "Internal server error") + ), + tag = "Auth", + summary = "Change currency preference", + description = "Updates the authenticated user's preferred display currency." +)] +pub async fn change_currency( + State(pool): State, + axum::Extension(claims): axum::Extension, + Json(payload): Json, +) -> Result, PaymeError> { + payload.validate()?; + + sqlx::query("UPDATE users SET currency = ? WHERE id = ?") + .bind(&payload.currency) + .bind(claims.sub) + .execute(&pool) + .await?; + + let user: (String,) = sqlx::query_as("SELECT username FROM users WHERE id = ?") + .bind(claims.sub) + .fetch_one(&pool) + .await?; + + Ok(Json(AuthResponse { + id: claims.sub, + username: user.0, + currency: payload.currency, + })) +} diff --git a/backend/src/lib.rs b/backend/src/lib.rs index f08cab3..47fdbc6 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -32,6 +32,7 @@ pub fn create_app(pool: SqlitePool) -> Router { .route("/api/auth/me", get(auth::me)) .route("/api/auth/change-username", put(auth::change_username)) .route("/api/auth/change-password", put(auth::change_password)) + .route("/api/auth/change-currency", put(auth::change_currency)) .route("/api/auth/clear-data", delete(auth::clear_all_data)) .route("/api/export", get(auth::export_db)) .route("/api/months", get(months::list_months)) diff --git a/backend/src/openapi/mod.rs b/backend/src/openapi/mod.rs index fdeb0f7..ac9274c 100644 --- a/backend/src/openapi/mod.rs +++ b/backend/src/openapi/mod.rs @@ -1,7 +1,7 @@ use utoipa::OpenApi; use crate::handlers::{ - auth::{AuthRequest, AuthResponse}, + auth::{AuthRequest, AuthResponse, ChangeCurrencyRequest}, budget::{CreateCategory, UpdateCategory, UpdateMonthlyBudget}, export::{ BudgetExport, CategoryExport, FixedExpenseExport, IncomeExport, ItemExport, MonthExport, @@ -24,6 +24,7 @@ use crate::models::{ crate::handlers::auth::login, crate::handlers::auth::logout, crate::handlers::auth::me, + crate::handlers::auth::change_currency, crate::handlers::export::export_json, crate::handlers::export::import_json, crate::handlers::budget::list_monthly_budgets, @@ -58,6 +59,7 @@ use crate::models::{ components(schemas( AuthRequest, AuthResponse, + ChangeCurrencyRequest, MonthlyBudget, UpdateMonthlyBudget, IncomeEntry, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 39129cc..bb1d5c3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -60,6 +60,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1775,6 +1776,7 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1840,6 +1842,7 @@ "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", @@ -2091,6 +2094,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2196,6 +2200,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2587,6 +2592,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3595,6 +3601,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3656,6 +3663,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3665,6 +3673,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3684,6 +3693,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -3746,7 +3756,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -3966,6 +3977,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4076,6 +4088,7 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -4197,6 +4210,7 @@ "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 01cc1cb..dbade0e 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -27,19 +27,19 @@ async function request( export const api = { auth: { register: (username: string, password: string) => - request<{ id: number; username: string }>("/auth/register", { + request<{ id: number; username: string; currency: string }>("/auth/register", { method: "POST", body: JSON.stringify({ username, password }), }), login: (username: string, password: string) => - request<{ id: number; username: string }>("/auth/login", { + request<{ id: number; username: string; currency: string }>("/auth/login", { method: "POST", body: JSON.stringify({ username, password }), }), logout: () => request("/auth/logout", { method: "POST" }), - me: () => request<{ id: number; username: string }>("/auth/me"), + me: () => request<{ id: number; username: string; currency: string }>("/auth/me"), changeUsername: (newUsername: string) => - request<{ id: number; username: string }>("/auth/change-username", { + request<{ id: number; username: string; currency: string }>("/auth/change-username", { method: "PUT", body: JSON.stringify({ new_username: newUsername }), }), @@ -48,6 +48,11 @@ export const api = { method: "PUT", body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }), }), + changeCurrency: (currency: string) => + request<{ id: number; username: string; currency: string }>("/auth/change-currency", { + method: "PUT", + body: JSON.stringify({ currency }), + }), clearAllData: (password: string) => request<{ message: string }>("/auth/clear-data", { method: "DELETE", diff --git a/frontend/src/components/BudgetSection.tsx b/frontend/src/components/BudgetSection.tsx index 7540659..aa3d9a0 100644 --- a/frontend/src/components/BudgetSection.tsx +++ b/frontend/src/components/BudgetSection.tsx @@ -6,6 +6,7 @@ import { Input } from "./ui/Input"; import { Button } from "./ui/Button"; import { ProgressBar } from "./ui/ProgressBar"; import { Modal } from "./ui/Modal"; +import { useCurrency } from "../hooks/useCurrency"; interface BudgetSectionProps { monthId: number; @@ -23,6 +24,7 @@ export function BudgetSection({ onUpdate, }: BudgetSectionProps) { const [isManaging, setIsManaging] = useState(false); + const { format } = useCurrency(); const [isAddingCategory, setIsAddingCategory] = useState(false); const [editingCategoryId, setEditingCategoryId] = useState(null); const [editingBudgetId, setEditingBudgetId] = useState(null); @@ -131,7 +133,7 @@ export function BudgetSection({
- ${budget.spent_amount.toFixed(2)} / ${budget.allocated_amount.toFixed(2)} + {format(budget.spent_amount)} / {format(budget.allocated_amount)} {!isReadOnly && (
))} @@ -98,7 +100,7 @@ export function FixedExpenses({ expenses, onUpdate }: FixedExpensesProps) { Total - ${total.toFixed(2)} + {format(total)} )} @@ -142,7 +144,7 @@ export function FixedExpenses({ expenses, onUpdate }: FixedExpensesProps) {
{expense.label}
- ${expense.amount.toFixed(2)} + {format(expense.amount)}
diff --git a/frontend/src/components/Summary.tsx b/frontend/src/components/Summary.tsx index 62b16ae..538c289 100644 --- a/frontend/src/components/Summary.tsx +++ b/frontend/src/components/Summary.tsx @@ -1,6 +1,7 @@ import { TrendingDown, Wallet, CreditCard, PiggyBank } from "lucide-react"; import { Card } from "./ui/Card"; import { ReactNode } from "react"; +import { useCurrency } from "../hooks/useCurrency"; interface SummaryProps { totalIncome: number; @@ -11,6 +12,7 @@ interface SummaryProps { } export function Summary({ totalIncome, totalFixed, totalSpent, remaining, extraCard }: SummaryProps) { + const { format } = useCurrency(); const isPositive = remaining >= 0; const items = [ @@ -53,7 +55,7 @@ export function Summary({ totalIncome, totalFixed, totalSpent, remaining, extraC {item.label}
- ${Math.abs(item.value).toFixed(2)} + {format(Math.abs(item.value))} {item.label === "Remaining" && item.value < 0 && ( deficit )} diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx index 9ca71a1..e5e0b5f 100644 --- a/frontend/src/context/AuthContext.tsx +++ b/frontend/src/context/AuthContext.tsx @@ -4,6 +4,7 @@ import { api } from "../api/client"; interface User { id: number; username: string; + currency: string; } interface AuthContextType { @@ -13,6 +14,7 @@ interface AuthContextType { register: (username: string, password: string) => Promise; logout: () => Promise; updateUsername: (username: string) => void; + updateCurrency: (currency: string) => void; } const AuthContext = createContext(undefined); @@ -53,11 +55,17 @@ export function AuthProvider({ children }: { children: ReactNode }) { } }; - return ( - - {children} - - ); + const updateCurrency = (currency: string) => { + if (user) { + setUser({ ...user, currency }); + } +}; + +return ( + + {children} + +); } export function useAuth() { diff --git a/frontend/src/hooks/useCurrency.ts b/frontend/src/hooks/useCurrency.ts new file mode 100644 index 0000000..314d768 --- /dev/null +++ b/frontend/src/hooks/useCurrency.ts @@ -0,0 +1,11 @@ +import { useAuth } from "../context/AuthContext"; +import { formatCurrency, Currency } from "../lib/currency"; + +export function useCurrency() { + const { user } = useAuth(); + const currency = (user?.currency as Currency) || "USD"; + + const format = (amount: number) => formatCurrency(amount, currency); + + return { currency, format }; +} \ No newline at end of file diff --git a/frontend/src/lib/currency.ts b/frontend/src/lib/currency.ts new file mode 100644 index 0000000..1090f06 --- /dev/null +++ b/frontend/src/lib/currency.ts @@ -0,0 +1,38 @@ +export const SUPPORTED_CURRENCIES = { + USD: { symbol: '$', name: 'US Dollar', locale: 'en-US' }, + EUR: { symbol: '€', name: 'Euro', locale: 'en-EU' }, + GBP: { symbol: '£', name: 'British Pound', locale: 'en-GB' }, + JPY: { symbol: '¥', name: 'Japanese Yen', locale: 'ja-JP' }, + CAD: { symbol: 'C$', name: 'Canadian Dollar', locale: 'en-CA' }, + AUD: { symbol: 'A$', name: 'Australian Dollar', locale: 'en-AU' }, + CHF: { symbol: 'CHF', name: 'Swiss Franc', locale: 'de-CH' }, + CNY: { symbol: '¥', name: 'Chinese Yuan', locale: 'zh-CN' }, + INR: { symbol: '₹', name: 'Indian Rupee', locale: 'en-IN' }, +} as const; + +export type Currency = keyof typeof SUPPORTED_CURRENCIES; + +export function formatCurrency( + amount: number, + currency: Currency = 'USD', + showSymbol: boolean = true +): string { + const currencyInfo = SUPPORTED_CURRENCIES[currency]; + + const formatted = new Intl.NumberFormat(currencyInfo.locale, { + style: 'currency', + currency: currency, + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(amount); + + if (!showSymbol) { + return formatted.replace(/[^\d,.-]/g, '').trim(); + } + + return formatted; +} + +export function getCurrencySymbol(currency: Currency = 'USD'): string { + return SUPPORTED_CURRENCIES[currency].symbol; +} \ No newline at end of file diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 2919f6a..21a07da 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -6,14 +6,19 @@ import { Modal } from "../components/ui/Modal"; import { useAuth } from "../context/AuthContext"; import { api } from "../api/client"; import { ArrowLeft } from "lucide-react"; +import { SUPPORTED_CURRENCIES, Currency } from "../lib/currency"; interface SettingsProps { onBack: () => void; } export function Settings({ onBack }: SettingsProps) { - const { user, logout, updateUsername } = useAuth(); + const { user, logout, updateUsername, updateCurrency } = useAuth(); const [newUsername, setNewUsername] = useState(user?.username || ""); + const [selectedCurrency, setSelectedCurrency] = useState((user?.currency as Currency) || "USD"); + const [currencyLoading, setCurrencyLoading] = useState(false); + const [currencyError, setCurrencyError] = useState(""); + const [currencySuccess, setCurrencySuccess] = useState(false); const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); @@ -51,6 +56,24 @@ export function Settings({ onBack }: SettingsProps) { } }; + const handleChangeCurrency = async (e: React.FormEvent) => { + e.preventDefault(); + setCurrencyError(""); + setCurrencySuccess(false); + + setCurrencyLoading(true); + try { + const response = await api.auth.changeCurrency(selectedCurrency); + updateCurrency(response.currency); + setCurrencySuccess(true); + setTimeout(() => setCurrencySuccess(false), 3000); + } catch { + setCurrencyError("Failed to change currency."); + } finally { + setCurrencyLoading(false); + } +}; + const handleChangePassword = async (e: React.FormEvent) => { e.preventDefault(); setPasswordError(""); @@ -142,6 +165,43 @@ export function Settings({ onBack }: SettingsProps) {

+ {/* Currency Preference Section */} +
+

+ Currency Preference +

+
+
+ + +

+ All amounts will be displayed in your selected currency +

+
+ {currencyError && ( +

{currencyError}

+ )} + {currencySuccess && ( +

Currency changed successfully

+ )} + +
+
Change Password

diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..396f082 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "payme-master", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From 8a313ce05b60fa111faa72b7fb706a48d03980de Mon Sep 17 00:00:00 2001 From: PeaTree-Maker Date: Sun, 18 Jan 2026 12:15:04 +0000 Subject: [PATCH 2/3] Adjust UI for currency preferences --- frontend/src/pages/Settings.tsx | 72 ++++++++++++++++----------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 21a07da..2bbd5ae 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -165,43 +165,43 @@ export function Settings({ onBack }: SettingsProps) {

- {/* Currency Preference Section */} -
-

- Currency Preference -

- -
- - -

- All amounts will be displayed in your selected currency -

-
- {currencyError && ( -

{currencyError}

- )} - {currencySuccess && ( -

Currency changed successfully

- )} - - + Currency Preference +

+
+
+ + +

+ All amounts will be displayed in your selected currency +

+ {currencyError && ( +

{currencyError}

+ )} + {currencySuccess && ( +

Currency changed successfully

+ )} + +
+
+ +
+

Change Password

From 34559f5539fe289ee35cf2f286721faa3b4aac35 Mon Sep 17 00:00:00 2001 From: PeaTree-Maker Date: Sun, 18 Jan 2026 21:07:56 +0000 Subject: [PATCH 3/3] fixes on feedback PR17 --- backend/src/handlers/auth.rs | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/backend/src/handlers/auth.rs b/backend/src/handlers/auth.rs index 31f8b9e..9ff1223 100644 --- a/backend/src/handlers/auth.rs +++ b/backend/src/handlers/auth.rs @@ -30,6 +30,11 @@ pub struct AuthResponse { pub currency: String, } +#[derive(Serialize, ToSchema)] +pub struct CurrencyChangeResponse { + pub currency: String, +} + #[utoipa::path( post, path = "/api/auth/register", @@ -181,7 +186,7 @@ pub async fn me( Ok(Json(AuthResponse { id: user.0, username: user.1, - currency: user.2, // ADDED + currency: user.2, })) } @@ -244,7 +249,6 @@ pub async fn change_username( .execute(&pool) .await?; - // ADDED: Fetch currency let currency: (String,) = sqlx::query_as("SELECT currency FROM users WHERE id = ?") .bind(claims.sub) .fetch_one(&pool) @@ -253,7 +257,7 @@ pub async fn change_username( Ok(Json(AuthResponse { id: claims.sub, username: payload.new_username, - currency: currency.0, // ADDED + currency: currency.0, })) } @@ -370,18 +374,28 @@ pub async fn clear_all_data( Json(serde_json::json!({"message": "All data cleared"})), )) } +const SUPPORTED_CURRENCIES: &[&str] = &["USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "CNY", "INR"]; + #[derive(Deserialize, ToSchema, Validate)] pub struct ChangeCurrencyRequest { - #[validate(length(min = 3, max = 3))] + #[validate(custom = "validate_currency")] pub currency: String, } +fn validate_currency(currency: &str) -> Result<(), validator::ValidationError> { + if SUPPORTED_CURRENCIES.contains(¤cy) { + Ok(()) + } else { + Err(validator::ValidationError::new("invalid_currency")) + } +} + #[utoipa::path( put, path = "/api/auth/change-currency", request_body = ChangeCurrencyRequest, responses( - (status = 200, description = "Currency changed successfully", body = AuthResponse), + (status = 200, description = "Currency changed successfully", body = CurrencyChangeResponse), (status = 500, description = "Internal server error") ), tag = "Auth", @@ -392,7 +406,7 @@ pub async fn change_currency( State(pool): State, axum::Extension(claims): axum::Extension, Json(payload): Json, -) -> Result, PaymeError> { +) -> Result, PaymeError> { payload.validate()?; sqlx::query("UPDATE users SET currency = ? WHERE id = ?") @@ -401,14 +415,7 @@ pub async fn change_currency( .execute(&pool) .await?; - let user: (String,) = sqlx::query_as("SELECT username FROM users WHERE id = ?") - .bind(claims.sub) - .fetch_one(&pool) - .await?; - - Ok(Json(AuthResponse { - id: claims.sub, - username: user.0, + Ok(Json(CurrencyChangeResponse { currency: payload.currency, })) }