Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions migrations/2026-04-24-000001_merchant_onboarding/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE users MODIFY COLUMN merchant_id VARCHAR(255) NOT NULL DEFAULT '';
DROP TABLE IF EXISTS user_merchants;
ALTER TABLE merchant_account DROP COLUMN merchant_name;
15 changes: 15 additions & 0 deletions migrations/2026-04-24-000001_merchant_onboarding/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
ALTER TABLE users MODIFY COLUMN merchant_id VARCHAR(255) NULL DEFAULT NULL;

CREATE TABLE user_merchants (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
merchant_id VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'admin',
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
UNIQUE KEY uk_user_merchant (user_id, merchant_id)
);

CREATE INDEX idx_user_merchants_user_id ON user_merchants (user_id);
CREATE INDEX idx_user_merchants_merchant_id ON user_merchants (merchant_id);

ALTER TABLE merchant_account ADD COLUMN merchant_name VARCHAR(255) NULL;
3 changes: 3 additions & 0 deletions migrations_pg/2026-04-24-000001_merchant_onboarding/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE users ALTER COLUMN merchant_id SET NOT NULL;
DROP TABLE IF EXISTS user_merchants;
ALTER TABLE merchant_account DROP COLUMN IF EXISTS merchant_name;
15 changes: 15 additions & 0 deletions migrations_pg/2026-04-24-000001_merchant_onboarding/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
ALTER TABLE users ALTER COLUMN merchant_id DROP NOT NULL;

CREATE TABLE user_merchants (
id BIGSERIAL PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
merchant_id VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'admin',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (user_id, merchant_id)
);

CREATE INDEX idx_user_merchants_user_id ON user_merchants (user_id);
CREATE INDEX idx_user_merchants_merchant_id ON user_merchants (merchant_id);

ALTER TABLE merchant_account ADD COLUMN merchant_name VARCHAR(255);
154 changes: 143 additions & 11 deletions scripts/test_auth.sh
Original file line number Diff line number Diff line change
Expand Up @@ -162,55 +162,179 @@ 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))
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" \
-H "Content-Type: application/json" \
-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))
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" \
-H "Content-Type: application/json" \
-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 ]"
Expand All @@ -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" \
Expand All @@ -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" \
Expand All @@ -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
Expand All @@ -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" \
Expand All @@ -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
Expand Down
11 changes: 10 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,16 @@ where
.route("/auth/signup", post(routes::user_auth::signup))
.route("/auth/login", post(routes::user_auth::login))
.route("/auth/logout", post(routes::user_auth::logout))
.route("/auth/me", get(routes::user_auth::me));
.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),
);

let router = axum::Router::new()
.merge(protected_router)
Expand Down
Loading
Loading