From a9fe6b29125631c560a88cd99f79664164e8bed1 Mon Sep 17 00:00:00 2001 From: Jagan Elavarasan Date: Tue, 21 Apr 2026 16:51:18 +0530 Subject: [PATCH 1/4] feat(dashboard): Hyperswitch-style UI refresh Align dashboard visual design with Hyperswitch design system: Inter font, blue brand color (#006df9), clean sidebar/topbar, card/button border-radius, and fix error state bleeding across Decision Explorer tabs. Co-Authored-By: Claude Sonnet 4.6 --- website/public/hyperswitch-icon.png | Bin 0 -> 9995 bytes website/src/components/layout/AppShell.tsx | 7 +- website/src/components/layout/Sidebar.tsx | 56 ++++---- .../components/pages/DecisionExplorerPage.tsx | 13 +- website/src/components/ui/Button.tsx | 12 +- website/src/components/ui/Card.tsx | 10 +- website/src/index.css | 129 +++--------------- website/tailwind.config.ts | 15 +- 8 files changed, 79 insertions(+), 163 deletions(-) create mode 100644 website/public/hyperswitch-icon.png diff --git a/website/public/hyperswitch-icon.png b/website/public/hyperswitch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c9a0fc50f7ad7fe7bd46f448f02b7521c53751ba GIT binary patch literal 9995 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 k7MrfRj_5o#w53U*v1{1>019{kDF6Tf literal 0 HcmV?d00001 diff --git a/website/src/components/layout/AppShell.tsx b/website/src/components/layout/AppShell.tsx index 7912a8f2..d8529294 100644 --- a/website/src/components/layout/AppShell.tsx +++ b/website/src/components/layout/AppShell.tsx @@ -4,12 +4,11 @@ import { TopBar } from './TopBar' export function AppShell() { return ( -
-
+
-
+
-
+
diff --git a/website/src/components/layout/Sidebar.tsx b/website/src/components/layout/Sidebar.tsx index 862d6e00..5a800aed 100644 --- a/website/src/components/layout/Sidebar.tsx +++ b/website/src/components/layout/Sidebar.tsx @@ -3,7 +3,6 @@ import { LayoutDashboard, GitBranch, Search, - Zap, TrendingUp, BookOpen, PieChart, @@ -12,81 +11,82 @@ import { export function Sidebar() { return ( -
)} diff --git a/website/src/pages/AuthPage.tsx b/website/src/pages/AuthPage.tsx index f9cb4545..4d0ff85b 100644 --- a/website/src/pages/AuthPage.tsx +++ b/website/src/pages/AuthPage.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { useNavigate } from 'react-router-dom' -import { useAuthStore } from '../store/authStore' +import { useAuthStore, MerchantInfo } from '../store/authStore' import { useMerchantStore } from '../store/merchantStore' import { apiFetch } from '../lib/api' import { Loader2, Eye, EyeOff, Lock } from 'lucide-react' @@ -11,6 +11,7 @@ interface AuthResponse { email: string merchant_id: string role: string + merchants: MerchantInfo[] } type Tab = 'login' | 'signup' @@ -23,7 +24,6 @@ export function AuthPage() { const [tab, setTab] = useState('login') const [email, setEmail] = useState('') const [password, setPassword] = useState('') - const [merchantId, setMerchantIdInput] = useState('') const [showPassword, setShowPassword] = useState(false) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) @@ -40,24 +40,23 @@ export function AuthPage() { try { const path = tab === 'login' ? '/auth/login' : '/auth/signup' - const body = - tab === 'login' - ? { email, password } - : { email, password, merchant_id: merchantId } - const res = await apiFetch(path, { method: 'POST', - body: JSON.stringify(body), + body: JSON.stringify({ email, password }), }) - setAuth(res.token, { - userId: res.user_id, - email: res.email, - merchantId: res.merchant_id, - role: res.role, - }) - setMerchantId(res.merchant_id) - navigate('/', { replace: true }) + setAuth( + res.token, + { userId: res.user_id, email: res.email, merchantId: res.merchant_id, role: res.role }, + res.merchants, + ) + if (res.merchant_id) setMerchantId(res.merchant_id) + + if (!res.merchant_id || res.merchants.length === 0) { + navigate('/onboarding', { replace: true }) + } else { + navigate('/', { replace: true }) + } } catch (err) { const msg = err instanceof Error ? err.message : 'Something went wrong' const match = msg.match(/API error \d+: (.+)/) @@ -332,54 +331,6 @@ export function AuthPage() { )}
- {/* Merchant ID (signup only) */} - {tab === 'signup' && ( -
- - setMerchantIdInput(e.target.value)} - placeholder="Enter your Merchant ID" - style={{ - width: '100%', - height: 40, - backgroundColor: 'rgb(255, 255, 255)', - border: '1px solid rgba(204, 210, 226, 0.75)', - borderRadius: 4, - padding: '0 8px', - fontSize: 14, - color: 'rgba(51, 51, 51, 0.75)', - outline: 'none', - boxSizing: 'border-box', - fontFamily: "'Inter', sans-serif", - }} - onFocus={(e) => { - e.target.style.border = '1px solid rgb(0, 109, 249)' - e.target.style.boxShadow = '0 0 0 2px rgba(0, 109, 249, 0.1)' - }} - onBlur={(e) => { - e.target.style.border = '1px solid rgba(204, 210, 226, 0.75)' - e.target.style.boxShadow = 'none' - }} - /> -

- The merchant account must already exist. -

-
- )} {/* Error */} {error && ( diff --git a/website/src/pages/OnboardingPage.tsx b/website/src/pages/OnboardingPage.tsx new file mode 100644 index 00000000..4b5c92dd --- /dev/null +++ b/website/src/pages/OnboardingPage.tsx @@ -0,0 +1,177 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { useAuthStore, MerchantInfo } from '../store/authStore' +import { useMerchantStore } from '../store/merchantStore' +import { apiFetch } from '../lib/api' +import { Loader2, Building2 } from 'lucide-react' + +interface CreateMerchantResponse { + token: string + merchant_id: string + merchant_name: string + merchants: MerchantInfo[] +} + +export function OnboardingPage() { + const navigate = useNavigate() + const { updateMerchant } = useAuthStore() + const { setMerchantId } = useMerchantStore() + + const [merchantName, setMerchantName] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError(null) + setLoading(true) + + try { + const res = await apiFetch('/onboarding/merchant', { + method: 'POST', + body: JSON.stringify({ merchant_name: merchantName }), + }) + + updateMerchant(res.token, res.merchant_id, res.merchants) + setMerchantId(res.merchant_id) + navigate('/', { replace: true }) + } catch (err) { + const msg = err instanceof Error ? err.message : 'Something went wrong' + const match = msg.match(/API error \d+: (.+)/) + if (match) { + try { + const parsed = JSON.parse(match[1]) + setError(parsed.message ?? msg) + } catch { + setError(match[1]) + } + } else { + setError(msg) + } + } finally { + setLoading(false) + } + } + + return ( +
+
+ {/* Header */} +
+
+ + + + + + + +
+ + Decision Engine + +
+ + {/* Body */} +
+

+
+ +
+ Create your merchant +

+

+ Set up your merchant account to start using Decision Engine. +

+ +
+
+ + setMerchantName(e.target.value)} + placeholder="e.g. Acme Corp" + style={{ + width: '100%', + height: 40, + backgroundColor: 'rgb(255, 255, 255)', + border: '1px solid rgba(204, 210, 226, 0.75)', + borderRadius: 4, + padding: '0 8px', + fontSize: 14, + color: 'rgba(51, 51, 51, 0.75)', + outline: 'none', + boxSizing: 'border-box', + fontFamily: "'Inter', sans-serif", + }} + onFocus={(e) => { + e.target.style.border = '1px solid rgb(0, 109, 249)' + e.target.style.boxShadow = '0 0 0 2px rgba(0, 109, 249, 0.1)' + }} + onBlur={(e) => { + e.target.style.border = '1px solid rgba(204, 210, 226, 0.75)' + e.target.style.boxShadow = 'none' + }} + /> +
+ + {error && ( +
+ {error} +
+ )} + + +
+
+
+
+ ) +} diff --git a/website/src/store/authStore.ts b/website/src/store/authStore.ts index b4db2b72..3f153e65 100644 --- a/website/src/store/authStore.ts +++ b/website/src/store/authStore.ts @@ -2,6 +2,12 @@ import { create } from 'zustand' import { persist } from 'zustand/middleware' import { tokenRef } from '../lib/tokenRef' +export interface MerchantInfo { + merchant_id: string + merchant_name: string + role: string +} + export interface AuthUser { userId: string email: string @@ -12,7 +18,9 @@ export interface AuthUser { interface AuthStore { token: string | null user: AuthUser | null - setAuth: (token: string, user: AuthUser) => void + merchants: MerchantInfo[] + setAuth: (token: string, user: AuthUser, merchants?: MerchantInfo[]) => void + updateMerchant: (token: string, merchantId: string, merchants: MerchantInfo[]) => void clearAuth: () => void } @@ -21,19 +29,27 @@ export const useAuthStore = create()( (set) => ({ token: null, user: null, - setAuth: (token, user) => { + merchants: [], + setAuth: (token, user, merchants = []) => { + tokenRef.set(token) + set({ token, user, merchants }) + }, + updateMerchant: (token, merchantId, merchants) => { tokenRef.set(token) - set({ token, user }) + set((state) => ({ + token, + merchants, + user: state.user ? { ...state.user, merchantId } : null, + })) }, clearAuth: () => { tokenRef.set(null) - set({ token: null, user: null }) + set({ token: null, user: null, merchants: [] }) }, }), { name: 'auth-store', onRehydrateStorage: () => (state) => { - // Restore token ref from persisted storage on page load if (state?.token) { tokenRef.set(state.token) } diff --git a/website/vite.config.ts b/website/vite.config.ts index 3a75f424..619d9487 100644 --- a/website/vite.config.ts +++ b/website/vite.config.ts @@ -176,6 +176,23 @@ export default defineConfig({ }) }, }, + '/onboarding': { + target: 'http://localhost:8080', + changeOrigin: true, + configure: (proxy) => { + proxy.on('proxyReq', (proxyReq, req) => { + console.log(`\n[PROXY] ${new Date().toISOString()}`) + console.log(`Forwarding: ${req.method} ${req.url} -> http://localhost:8080${req.url}`) + }) + proxy.on('proxyRes', (proxyRes, req) => { + console.log(`[PROXY] Response: ${proxyRes.statusCode} ${proxyRes.statusMessage} for ${req.url}`) + }) + proxy.on('error', (err, req) => { + console.log(`\n[PROXY ERROR] ${new Date().toISOString()}`) + console.log(`Error forwarding ${req.url}:`, err.message) + }) + }, + }, }, fs: { strict: false, From 6189bd80534e93df471e03c109ae86ed79045013 Mon Sep 17 00:00:00 2001 From: Jagan Elavarasan Date: Fri, 24 Apr 2026 12:52:46 +0530 Subject: [PATCH 3/4] update test script --- scripts/test_auth.sh | 154 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 143 insertions(+), 11 deletions(-) diff --git a/scripts/test_auth.sh b/scripts/test_auth.sh index 17f4d00a..4baa7f64 100755 --- a/scripts/test_auth.sh +++ b/scripts/test_auth.sh @@ -162,14 +162,16 @@ if [ -n "$NEW_KEY_ID" ]; then fi fi -# ── 8. User signup ───────────────────────────────────── +# ── 8. User signup (no merchant_id required) ─────────── echo "" echo "[ User signup ]" SIGNUP_RESPONSE=$(curl -s -X POST "$BASE_URL/auth/signup" \ -H "Content-Type: application/json" \ - -d "{\"email\": \"$TEST_EMAIL\", \"password\": \"$TEST_PASSWORD\", \"merchant_id\": \"$MERCHANT_ID\"}") + -d "{\"email\": \"$TEST_EMAIL\", \"password\": \"$TEST_PASSWORD\"}") JWT_TOKEN=$(echo "$SIGNUP_RESPONSE" | grep -o '"token":"[^"]*"' | cut -d'"' -f4) +SIGNUP_MERCHANT_ID=$(echo "$SIGNUP_RESPONSE" | grep -o '"merchant_id":"[^"]*"' | cut -d'"' -f4) +SIGNUP_MERCHANTS=$(echo "$SIGNUP_RESPONSE" | grep -o '"merchants":\[\]') if [ -n "$JWT_TOKEN" ]; then echo " PASS POST /auth/signup returns JWT token" PASS=$((PASS + 1)) @@ -177,16 +179,130 @@ else echo " FAIL POST /auth/signup — unexpected response: $SIGNUP_RESPONSE" FAIL=$((FAIL + 1)) fi +if [ -z "$SIGNUP_MERCHANT_ID" ] || [ "$SIGNUP_MERCHANT_ID" = '""' ]; then + echo " PASS POST /auth/signup — merchant_id is empty (onboarding pending)" + PASS=$((PASS + 1)) +else + echo " INFO POST /auth/signup — merchant_id: $SIGNUP_MERCHANT_ID" +fi +if [ -n "$SIGNUP_MERCHANTS" ]; then + echo " PASS POST /auth/signup — merchants list is empty" + PASS=$((PASS + 1)) +else + echo " FAIL POST /auth/signup — expected empty merchants list, got: $SIGNUP_RESPONSE" + FAIL=$((FAIL + 1)) +fi # ── 9. Duplicate signup → 409 ───────────────────────── echo "" echo "[ Duplicate signup ]" STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/auth/signup" \ -H "Content-Type: application/json" \ - -d "{\"email\": \"$TEST_EMAIL\", \"password\": \"$TEST_PASSWORD\", \"merchant_id\": \"$MERCHANT_ID\"}") + -d "{\"email\": \"$TEST_EMAIL\", \"password\": \"$TEST_PASSWORD\"}") check "Duplicate email → 409" "409" "$STATUS" -# ── 10. Login ────────────────────────────────────────── +# ── 10. Onboarding: create first merchant ────────────── +echo "" +echo "[ Onboarding: create merchant ]" +ONBOARD_RESPONSE=$(curl -s -X POST "$BASE_URL/onboarding/merchant" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $JWT_TOKEN" \ + -d "{\"merchant_name\": \"Test Corp\"}") + +ONBOARD_TOKEN=$(echo "$ONBOARD_RESPONSE" | grep -o '"token":"[^"]*"' | cut -d'"' -f4) +ONBOARD_MID=$(echo "$ONBOARD_RESPONSE" | grep -o '"merchant_id":"[^"]*"' | head -1 | cut -d'"' -f4) +ONBOARD_NAME=$(echo "$ONBOARD_RESPONSE" | grep -o '"merchant_name":"[^"]*"' | head -1 | cut -d'"' -f4) + +if [ -n "$ONBOARD_TOKEN" ] && [ -n "$ONBOARD_MID" ]; then + echo " PASS POST /onboarding/merchant returns token + merchant_id" + echo " merchant_id: $ONBOARD_MID" + PASS=$((PASS + 1)) +else + echo " FAIL POST /onboarding/merchant — unexpected response: $ONBOARD_RESPONSE" + FAIL=$((FAIL + 1)) +fi +check "Merchant name stored correctly" "Test Corp" "$ONBOARD_NAME" + +ONBOARD_COUNT=$(echo "$ONBOARD_RESPONSE" | grep -o '"merchant_id"' | wc -l | tr -d ' ') +if [ "$ONBOARD_COUNT" -ge "1" ]; then + echo " PASS POST /onboarding/merchant — merchants list has 1 entry" + PASS=$((PASS + 1)) +else + echo " FAIL POST /onboarding/merchant — merchants list empty: $ONBOARD_RESPONSE" + FAIL=$((FAIL + 1)) +fi + +# ── 11. Onboarding: create second merchant ───────────── +echo "" +echo "[ Onboarding: create second merchant ]" +ONBOARD2_RESPONSE=$(curl -s -X POST "$BASE_URL/onboarding/merchant" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ONBOARD_TOKEN" \ + -d "{\"merchant_name\": \"Beta Inc\"}") + +ONBOARD2_MID=$(echo "$ONBOARD2_RESPONSE" | grep -o '"merchant_id":"[^"]*"' | head -1 | cut -d'"' -f4) +ONBOARD2_COUNT=$(echo "$ONBOARD2_RESPONSE" | grep -o '"merchant_id"' | wc -l | tr -d ' ') +ONBOARD2_TOKEN=$(echo "$ONBOARD2_RESPONSE" | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4) + +if [ -n "$ONBOARD2_MID" ]; then + echo " PASS POST /onboarding/merchant — second merchant created" + echo " merchant_id: $ONBOARD2_MID" + PASS=$((PASS + 1)) +else + echo " FAIL POST /onboarding/merchant (2nd) — unexpected: $ONBOARD2_RESPONSE" + FAIL=$((FAIL + 1)) +fi +if [ "$ONBOARD2_COUNT" -ge "2" ]; then + echo " PASS merchants list now has $ONBOARD2_COUNT entries" + PASS=$((PASS + 1)) +else + echo " FAIL Expected 2+ merchants in list, got $ONBOARD2_COUNT: $ONBOARD2_RESPONSE" + FAIL=$((FAIL + 1)) +fi + +# ── 12. List merchants ───────────────────────────────── +echo "" +echo "[ List merchants ]" +LIST_MERCHANTS=$(curl -s "$BASE_URL/auth/merchants" \ + -H "Authorization: Bearer $ONBOARD2_TOKEN") +LIST_COUNT=$(echo "$LIST_MERCHANTS" | grep -o '"merchant_id"' | wc -l | tr -d ' ') +if [ "$LIST_COUNT" -ge "2" ]; then + echo " PASS GET /auth/merchants returns $LIST_COUNT merchants" + PASS=$((PASS + 1)) +else + echo " FAIL GET /auth/merchants — got: $LIST_MERCHANTS" + FAIL=$((FAIL + 1)) +fi + +# ── 13. Switch merchant ──────────────────────────────── +echo "" +echo "[ Switch merchant ]" +SWITCH_RESPONSE=$(curl -s --max-time 10 -X POST "$BASE_URL/auth/switch-merchant" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ONBOARD2_TOKEN" \ + -d "{\"merchant_id\": \"$ONBOARD_MID\"}") + +SWITCH_TOKEN=$(echo "$SWITCH_RESPONSE" | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4) +SWITCH_MID=$(echo "$SWITCH_RESPONSE" | grep -o '"merchant_id":"[^"]*"' | head -1 | cut -d'"' -f4) + +if [ -n "$SWITCH_TOKEN" ]; then + echo " PASS POST /auth/switch-merchant returns new token" + PASS=$((PASS + 1)) +else + echo " FAIL POST /auth/switch-merchant — unexpected: $SWITCH_RESPONSE" + FAIL=$((FAIL + 1)) +fi +check "Switch sets correct active merchant_id" "$ONBOARD_MID" "$SWITCH_MID" + +echo "" +echo "[ Switch to non-existent merchant → 404 ]" +STATUS=$(curl -s --max-time 10 -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/auth/switch-merchant" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ONBOARD2_TOKEN" \ + -d '{"merchant_id": "merchant_doesnotexist"}') +check "Switch to unknown merchant → 404" "404" "$STATUS" + +# ── 14. Login returns merchants list ────────────────── echo "" echo "[ User login ]" LOGIN_RESPONSE=$(curl -s -X POST "$BASE_URL/auth/login" \ @@ -194,6 +310,7 @@ LOGIN_RESPONSE=$(curl -s -X POST "$BASE_URL/auth/login" \ -d "{\"email\": \"$TEST_EMAIL\", \"password\": \"$TEST_PASSWORD\"}") JWT_TOKEN=$(echo "$LOGIN_RESPONSE" | grep -o '"token":"[^"]*"' | cut -d'"' -f4) +LOGIN_MERCHANT_COUNT=$(echo "$LOGIN_RESPONSE" | grep -o '"merchant_id"' | wc -l | tr -d ' ') if [ -n "$JWT_TOKEN" ]; then echo " PASS POST /auth/login returns JWT token" PASS=$((PASS + 1)) @@ -201,8 +318,15 @@ else echo " FAIL POST /auth/login — unexpected response: $LOGIN_RESPONSE" FAIL=$((FAIL + 1)) fi +if [ "$LOGIN_MERCHANT_COUNT" -ge "2" ]; then + echo " PASS POST /auth/login — merchants list populated ($LOGIN_MERCHANT_COUNT entries)" + PASS=$((PASS + 1)) +else + echo " FAIL POST /auth/login — merchants list missing or empty: $LOGIN_RESPONSE" + FAIL=$((FAIL + 1)) +fi -# ── 11. Wrong password → 401 ────────────────────────── +# ── 15. Wrong password → 401 ────────────────────────── echo "" echo "[ Wrong password ]" STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/auth/login" \ @@ -210,7 +334,7 @@ STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/auth/login" \ -d "{\"email\": \"$TEST_EMAIL\", \"password\": \"wrongpassword\"}") check "Wrong password → 401" "401" "$STATUS" -# ── 12. JWT accesses protected route ────────────────── +# ── 16. JWT accesses protected route ────────────────── if [ -n "$JWT_TOKEN" ]; then echo "" echo "[ JWT auth on protected routes ]" @@ -230,7 +354,7 @@ if [ -n "$JWT_TOKEN" ]; then echo " INFO Auth not enforced — skipping invalid JWT check" fi - # ── 13. /auth/me ────────────────────────────────── + # ── 17. /auth/me ────────────────────────────────── echo "" echo "[ /auth/me ]" ME_RESPONSE=$(curl -s "$BASE_URL/auth/me" \ @@ -242,8 +366,16 @@ if [ -n "$JWT_TOKEN" ]; then echo " FAIL GET /auth/me — got: $ME_RESPONSE" FAIL=$((FAIL + 1)) fi + ME_MERCHANT_COUNT=$(echo "$ME_RESPONSE" | grep -o '"merchant_id"' | wc -l | tr -d ' ') + if [ "$ME_MERCHANT_COUNT" -ge "2" ]; then + echo " PASS GET /auth/me — merchants list populated" + PASS=$((PASS + 1)) + else + echo " FAIL GET /auth/me — merchants list missing: $ME_RESPONSE" + FAIL=$((FAIL + 1)) + fi - # ── 14. Logout ──────────────────────────────────── + # ── 18. Logout ──────────────────────────────────── echo "" echo "[ Logout ]" LOGOUT_RESPONSE=$(curl -s -X POST "$BASE_URL/auth/logout" \ @@ -256,7 +388,7 @@ if [ -n "$JWT_TOKEN" ]; then FAIL=$((FAIL + 1)) fi - # ── 15. Revoked JWT rejected ────────────────────── + # ── 19. Revoked JWT rejected ────────────────────── echo "" echo "[ Revoked JWT rejected ]" sleep 1 @@ -270,7 +402,7 @@ if [ -n "$JWT_TOKEN" ]; then echo " INFO Auth not enforced — skipping revoked JWT check" fi - # ── 16. Re-login after logout ───────────────────── + # ── 20. Re-login after logout ───────────────────── echo "" echo "[ Re-login after logout ]" RELOGIN_RESPONSE=$(curl -s -X POST "$BASE_URL/auth/login" \ @@ -286,7 +418,7 @@ if [ -n "$JWT_TOKEN" ]; then fi fi -# ── 17. Redis cache hit (API key) ────────────────────── +# ── 21. Redis cache hit (API key) ────────────────────── echo "" echo "[ Redis cache hit ]" for i in 1 2; do From 6f88147459c34f054f682e8eff6d8317e71e9766 Mon Sep 17 00:00:00 2001 From: Jagan Elavarasan Date: Fri, 24 Apr 2026 12:56:09 +0530 Subject: [PATCH 4/4] code formatting --- src/app.rs | 10 ++++++-- src/routes/user_auth.rs | 57 +++++++++++++++++++++++++++-------------- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/app.rs b/src/app.rs index 161578c8..6566534c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -283,8 +283,14 @@ where .route("/auth/logout", post(routes::user_auth::logout)) .route("/auth/me", get(routes::user_auth::me)) .route("/auth/merchants", get(routes::user_auth::list_merchants)) - .route("/auth/switch-merchant", post(routes::user_auth::switch_merchant)) - .route("/onboarding/merchant", post(routes::user_auth::create_merchant)); + .route( + "/auth/switch-merchant", + post(routes::user_auth::switch_merchant), + ) + .route( + "/onboarding/merchant", + post(routes::user_auth::create_merchant), + ); let router = axum::Router::new() .merge(protected_router) diff --git a/src/routes/user_auth.rs b/src/routes/user_auth.rs index a3773076..d095f32c 100644 --- a/src/routes/user_auth.rs +++ b/src/routes/user_auth.rs @@ -98,7 +98,9 @@ pub async fn signup( .map_err(|_| UserAuthError::StorageError)?; if !existing.is_empty() { - return Err(error::ContainerError::from(UserAuthError::EmailAlreadyExists)); + return Err(error::ContainerError::from( + UserAuthError::EmailAlreadyExists, + )); } let password_hash = @@ -118,7 +120,11 @@ pub async fn signup( #[cfg(feature = "postgres")] is_active: true, #[cfg(feature = "mysql")] - email_verified: if global_config.user_auth.email_verification_enabled { 0 } else { 1 }, + email_verified: if global_config.user_auth.email_verification_enabled { + 0 + } else { + 1 + }, #[cfg(feature = "postgres")] email_verified: !global_config.user_auth.email_verification_enabled, created_at: now, @@ -173,9 +179,13 @@ pub async fn login( let is_active = { #[cfg(feature = "mysql")] - { user.is_active != 0 } + { + user.is_active != 0 + } #[cfg(feature = "postgres")] - { user.is_active } + { + user.is_active + } }; if !is_active { return Err(error::ContainerError::from(UserAuthError::AccountInactive)); @@ -183,9 +193,13 @@ pub async fn login( let email_verified = { #[cfg(feature = "mysql")] - { user.email_verified != 0 } + { + user.email_verified != 0 + } #[cfg(feature = "postgres")] - { user.email_verified } + { + user.email_verified + } }; if global_config.user_auth.email_verification_enabled && !email_verified { return Err(error::ContainerError::from(UserAuthError::EmailNotVerified)); @@ -198,12 +212,12 @@ pub async fn login( } let merchants = fetch_user_merchants(&app_state, &user.user_id).await?; - let active_merchant_id = user - .merchant_id - .clone() - .unwrap_or_else(|| { - merchants.first().map(|m| m.merchant_id.clone()).unwrap_or_default() - }); + let active_merchant_id = user.merchant_id.clone().unwrap_or_else(|| { + merchants + .first() + .map(|m| m.merchant_id.clone()) + .unwrap_or_default() + }); let token = auth::generate_jwt( &user.user_id, @@ -277,7 +291,11 @@ pub async fn create_merchant( #[cfg(feature = "postgres")] use crate::storage::schema_pg::users::dsl as u_dsl; - let conn = &app_state.db.get_conn().await.map_err(|_| UserAuthError::StorageError)?; + let conn = &app_state + .db + .get_conn() + .await + .map_err(|_| UserAuthError::StorageError)?; crate::generics::generic_update_if_present::< ::Table, UserMerchantIdUpdate, @@ -285,7 +303,9 @@ pub async fn create_merchant( >( conn, u_dsl::user_id.eq(claims.user_id.clone()), - UserMerchantIdUpdate { merchant_id: Some(merchant_id.clone()) }, + UserMerchantIdUpdate { + merchant_id: Some(merchant_id.clone()), + }, ) .await .map_err(|_| UserAuthError::StorageError)?; @@ -396,7 +416,9 @@ pub async fn logout( .await; } - Ok(Json(serde_json::json!({ "message": "Logged out successfully" }))) + Ok(Json( + serde_json::json!({ "message": "Logged out successfully" }), + )) } #[axum::debug_handler] @@ -448,10 +470,7 @@ async fn fetch_user_merchants( ::Table, _, UserMerchant, - >( - &app_state.db, - um_dsl::user_id.eq(user_id.clone()), - ) + >(&app_state.db, um_dsl::user_id.eq(user_id.clone())) .await .map_err(|_| UserAuthError::StorageError)?;