Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
5bcb1be
Create architecture docs for supabase auth implementation
ebma Oct 31, 2025
707d11d
Implement Supabase authentication routes and user ID tracking in models
ebma Oct 31, 2025
91fbb4a
Add user model and establish associations with other entities
ebma Oct 31, 2025
d649c08
Add initial configuration files and scripts for project setup
ebma Oct 31, 2025
531564f
Implement authentication flow with email verification and OTP handling
ebma Oct 31, 2025
82e1cd8
Add email and OTP authentication steps to the user flow
ebma Oct 31, 2025
693cff7
Merge branch 'staging' into 882-account-creation-on-vortex
ebma Dec 3, 2025
eee8f73
Update @supabase/supabase-js dependency to use catalog version
ebma Dec 3, 2025
f88ea27
Add userId to registerRamp functionality for enhanced user tracking
ebma Dec 3, 2025
b30a9a8
Fix tax_ids migration to safely rename column and add enum value
ebma Dec 3, 2025
f8a1670
Add userId extraction and handling in quote and ramp routes for impro…
ebma Dec 3, 2025
de2e6bf
Refactor useAuthTokens to accept actorRef and update ramp machine for…
ebma Dec 3, 2025
063ac40
Fix quote not generated if user not logged in
ebma Dec 5, 2025
7cc19e9
Fix email change button not working
ebma Dec 5, 2025
b7ed019
Fix session refresh not working
ebma Dec 5, 2025
5c2443e
Rename authTokens to userSessionTokens
ebma Dec 5, 2025
8b1af39
Move auth before details page
ebma Dec 5, 2025
267cdca
Fix initial auth state
ebma Dec 5, 2025
b0cf245
Change stepper
ebma Dec 5, 2025
e328411
Adjust styling of auth cards
ebma Dec 5, 2025
8bf845b
Add logout feature to details page
ebma Dec 5, 2025
a1c28f5
Fix email not stored
ebma Dec 5, 2025
5a912d4
Adjust magic link template
ebma Dec 5, 2025
06f7958
Adjust magic link template again
ebma Dec 5, 2025
a52726d
Make quote summary always expand over card content itself
ebma Dec 8, 2025
b005ecb
Make quote summary also overlap on other cards
ebma Dec 8, 2025
54aa972
Adjust spacing of card content
ebma Dec 8, 2025
260935f
Refactor layout and improve z-index for QuoteSummary in multiple comp…
ebma Dec 9, 2025
e16f378
Refactor to deduplicate
ebma Dec 9, 2025
0834101
Merge branch 'staging' into 882-account-creation-on-vortex
ebma Dec 9, 2025
901733e
Add revertMigration function to handle specific migration rollbacks
ebma Dec 9, 2025
a6f8d0e
Adjust migrations to properly apply to existing data
ebma Dec 9, 2025
b1d23bf
Add proxy to migrator script
ebma Dec 9, 2025
8841ca9
Make AuthOTPStep responsive
ebma Dec 10, 2025
7f48fac
Fix issue with otp fields not cleared on second error
ebma Dec 10, 2025
44de0e3
Add signup email template
ebma Dec 10, 2025
5cea2fa
Adjust magic_link.html template
ebma Dec 10, 2025
c9e82d0
Adjust 022-add-user-id-to-entities.ts to prevent losing data on down()
ebma Dec 10, 2025
133a5ff
Fix other bug in 022 migration
ebma Dec 10, 2025
c1c0ae0
Fix type issues
ebma Dec 10, 2025
f330aa6
Use cn()
ebma Dec 11, 2025
41c4482
Remove userSessionTokens from ramp machine
ebma Dec 11, 2025
a130238
Refactor layout by removing unnecessary overflow-y-auto classes in Av…
ebma Dec 12, 2025
32ae0cb
Make sure quote is persisted on rampState machine
ebma Dec 12, 2025
d6886d9
Try to fix the spacing between action button and quote summary
ebma Dec 12, 2025
12de1b8
Use different method to position widget action and quote summary
ebma Dec 12, 2025
f251695
Fix access to userId from requests
ebma Dec 12, 2025
cf59dae
Allow rampState userId to be null on database model
ebma Dec 15, 2025
e24ed22
Adjust the spacing below the actions button
ebma Dec 15, 2025
15b845c
Fix stepper phases
ebma Dec 15, 2025
ffb2a70
Remove automatic scroll on quote summary
ebma Dec 15, 2025
eaae3e5
Fix issue with quote not set on ramp state when starting on email flow
ebma Dec 15, 2025
c8e91a1
Fix action button alignment in AveniaLivenessStep
ebma Dec 15, 2025
daa2475
Rename 'users' table to 'profiles' to adhere to Supabase best practice
ebma Dec 15, 2025
5197de3
Merge architecture documents for auth feature
ebma Dec 15, 2025
7211e32
Initial plan
Copilot Dec 15, 2025
5f979be
Address code review comments: logging, PostgreSQL compatibility, rate…
Copilot Dec 15, 2025
b8125dd
Fix code review issues: truncate auth header in logs, use ref to prev…
Copilot Dec 15, 2025
8a8169c
Refactor URL token handling in authentication flow to set Supabase se…
ebma Dec 15, 2025
20d0c5f
Refactor email input form structure and styling for improved accessib…
ebma Dec 15, 2025
ed92096
Replace console log with logger
ebma Dec 15, 2025
22671a6
Refactor authentication token handling to use camelCase properties fo…
ebma Dec 15, 2025
ca868ef
Configure supabase signup template in config
ebma Dec 16, 2025
70568bd
Merge pull request #969 from pendulum-chain/copilot/sub-pr-912
ebma Dec 16, 2025
2fbed83
Add doc for supabase auth
ebma Dec 23, 2025
64697b0
Merge branch 'staging' into 882-account-creation-on-vortex
ebma Jan 9, 2026
95d2f33
Fix type issues after merge
ebma Jan 12, 2026
54281ec
Fix email not set correctly on ramp state
ebma Jan 12, 2026
d2de645
Fix issue with local templates
ebma Jan 12, 2026
1e969d4
Enhance authentication middleware and enforce user authentication for…
ebma Jan 12, 2026
91ac142
Add checkbox for t&c
ebma Jan 12, 2026
f703c9d
Refactor KYC user ID handling to allow null values and update routes …
ebma Jan 12, 2026
a8f8592
Allow continuing in 'Enter details' page even if quote expired
ebma Jan 12, 2026
aec2a45
Merge branch 'staging' into 882-account-creation-on-vortex
ebma Jan 13, 2026
c70f78d
Add more translations for other widget cards
ebma Jan 13, 2026
0999560
Fix user can't proceed on 'enter details' page if quote expired
ebma Jan 13, 2026
fc797bd
Change go-back behaviour on payment summary to go back to 'enter deta…
ebma Jan 13, 2026
e5480d0
Remove comments
ebma Jan 13, 2026
35b6b07
implement InputOTP
Sharqiewicz Jan 16, 2026
9175421
Merge branch '882-account-creation-on-vortex' of github.com:pendulum-…
Sharqiewicz Jan 16, 2026
17dc7d5
unify styles of email input
Sharqiewicz Jan 16, 2026
8730b25
lint fixes
Sharqiewicz Jan 18, 2026
484108d
improve email validation
Sharqiewicz Jan 18, 2026
652d111
improve email validation
Sharqiewicz Jan 18, 2026
2ee1d77
simplify QuoteSummary animations
Sharqiewicz Jan 18, 2026
ed122c6
fix partnerApiKeys type issue
Sharqiewicz Jan 19, 2026
6416dd1
remove unused shadcn config
Sharqiewicz Jan 19, 2026
2f2b0d7
lint fixes
Sharqiewicz Jan 19, 2026
075ef08
remove unused vars
Sharqiewicz Jan 19, 2026
849a5ad
remove duplicated inline style for quote summary
Sharqiewicz Jan 20, 2026
ac28722
change quote summary layout height
Sharqiewicz Jan 20, 2026
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
5 changes: 5 additions & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ SANDBOX_ENABLED=false
# Example: openssl rand -base64 32
ADMIN_SECRET=your-secure-admin-secret-here

# Supabase Configuration
SUPABASE_URL=https://your-project-id.supabase.co
SUPABASE_ANON_KEY=your-anon-key-here
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here

# Database
DB_HOST=localhost
DB_PORT=5432
Expand Down
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"@polkadot/util": "catalog:",
"@polkadot/util-crypto": "catalog:",
"@scure/bip39": "^1.5.4",
"@supabase/supabase-js": "^2.87.1",
"@supabase/supabase-js": "catalog:",
"@vortexfi/shared": "workspace:*",
"@wagmi/core": "catalog:",
"axios": "catalog:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { generateApiKey, getKeyPrefix, hashApiKey } from "../../middlewares/apiK
*/
export async function createApiKey(req: Request, res: Response): Promise<void> {
try {
const { partnerName } = req.params;
const { partnerName } = req.params as { partnerName: string };
const { name, expiresAt } = req.body;

// Verify at least one partner with this name exists and is active
Expand Down Expand Up @@ -78,7 +78,7 @@ export async function createApiKey(req: Request, res: Response): Promise<void> {
expiresAt: expirationDate,
isActive: true,
partnerCount: partners.length,
partnerName: partnerName,
partnerName,
publicKey: {
id: publicKeyRecord.id,
key: publicKey, // Can be shown anytime (it's public)
Expand Down Expand Up @@ -112,7 +112,7 @@ export async function createApiKey(req: Request, res: Response): Promise<void> {
*/
export async function listApiKeys(req: Request, res: Response): Promise<void> {
try {
const { partnerName } = req.params;
const { partnerName } = req.params as { partnerName: string };

// Verify partner exists
const partners = await Partner.findAll({
Expand Down
162 changes: 162 additions & 0 deletions apps/api/src/api/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Request, Response } from "express";
import User from "../../models/user.model";
import { SupabaseAuthService } from "../services/auth";

export class AuthController {
/**
* Check if email is registered
* GET /api/v1/auth/check-email?email=user@example.com
*/
static async checkEmail(req: Request, res: Response) {
try {
const { email } = req.query;

if (!email || typeof email !== "string") {
return res.status(400).json({
error: "Email is required"
});
}

const exists = await SupabaseAuthService.checkUserExists(email);

return res.json({
action: exists ? "signin" : "signup",
exists
});
} catch (error) {
console.error("Error in checkEmail:", error);
return res.status(500).json({
error: "Failed to check email"
});
}
}

/**
* Request OTP
* POST /api/v1/auth/request-otp
*/
static async requestOTP(req: Request, res: Response) {
try {
const { email } = req.body;

if (!email) {
return res.status(400).json({
error: "Email is required"
});
}

await SupabaseAuthService.sendOTP(email);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this operation is stateless on the backend, right? Maybe we could add an entry to the users table, to mark the last OTP timestamp or any other metadata.

In case we loose the state in the UI or we want to keep track.


return res.json({
message: "OTP sent to email",
success: true
});
} catch (error) {
console.error("Error in requestOTP:", error);
return res.status(500).json({
error: "Failed to send OTP"
});
}
}

/**
* Verify OTP
* POST /api/v1/auth/verify-otp
*/
static async verifyOTP(req: Request, res: Response) {
try {
const { email, token } = req.body;

if (!email || !token) {
return res.status(400).json({
error: "Email and token are required"
});
}

const result = await SupabaseAuthService.verifyOTP(email, token);

// Sync user to local database (upsert)
await User.upsert({
email: email,
id: result.user_id
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the upsert only for handling new users? I assume the userId should never change but I may be wrong.

});

return res.json({
access_token: result.access_token,
refresh_token: result.refresh_token,
success: true,
user_id: result.user_id
});
} catch (error) {
console.error("Error in verifyOTP:", error);
return res.status(400).json({
error: "Invalid OTP or OTP expired"
});
}
}

/**
* Refresh token
* POST /api/v1/auth/refresh
*/
static async refreshToken(req: Request, res: Response) {
try {
const { refresh_token } = req.body;

if (!refresh_token) {
return res.status(400).json({
error: "Refresh token is required"
});
}

const result = await SupabaseAuthService.refreshToken(refresh_token);

return res.json({
access_token: result.access_token,
refresh_token: result.refresh_token,
success: true
});
} catch (error) {
console.error("Error in refreshToken:", error);
return res.status(401).json({
error: "Invalid refresh token"
});
}
}

/**
* Verify token
* POST /api/v1/auth/verify
*/
static async verifyToken(req: Request, res: Response) {
try {
const { access_token } = req.body;

if (!access_token) {
return res.status(400).json({
error: "Access token is required"
});
}

const result = await SupabaseAuthService.verifyToken(access_token);

if (!result.valid) {
return res.status(401).json({
error: "Invalid token",
valid: false
});
}

return res.json({
user_id: result.user_id,
valid: true
});
} catch (error) {
console.error("Error in verifyToken:", error);
return res.status(401).json({
error: "Token verification failed",
valid: false
});
}
}
}
9 changes: 7 additions & 2 deletions apps/api/src/api/controllers/brla.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,12 +203,14 @@ export const recordInitialKycAttempt = async (
taxId
}
});

if (!taxIdRecord) {
const accountType = isValidCnpj(taxId)
? AveniaAccountType.COMPANY
: isValidCpf(taxId)
? AveniaAccountType.INDIVIDUAL
: undefined;

// Create the entry only if a valid taxId is provided. Otherwise we ignore the request.
if (accountType) {
await TaxId.create({
Expand All @@ -217,7 +219,8 @@ export const recordInitialKycAttempt = async (
initialSessionId: sessionId ?? null,
internalStatus: TaxIdInternalStatus.Consulted,
subAccountId: "",
taxId
taxId,
userId: req.userId ?? null
});
}
}
Expand Down Expand Up @@ -317,14 +320,16 @@ export const createSubaccount = async (
} else {
// The entry should have been created the very first a new cpf/cnpj is consulted.
// We leave this as is for now to avoid breaking changes.

await TaxId.create({
accountType,
initialQuoteId: quoteId,
initialSessionId: sessionId ?? null,
internalStatus: TaxIdInternalStatus.Requested,
requestedDate: new Date(),
subAccountId: id,
taxId: taxId
taxId: taxId,
userId: req.userId ?? null
});
}

Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/api/controllers/maintenance.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const getAllMaintenanceSchedules: RequestHandler = async (_, res) => {
*/
export const updateScheduleActiveStatus: RequestHandler = async (req, res) => {
try {
const { id } = req.params;
const { id } = req.params as { id: string };
const { isActive } = req.body;

if (typeof isActive !== "boolean") {
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/api/controllers/metrics.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ let supabaseClient: SupabaseClient | null = null;

function getSupabaseClient() {
if (!supabaseClient) {
if (!config.supabaseUrl) {
if (!config.supabase.url) {
throw new Error("Missing Supabase URL in configuration.");
}
if (!config.supabaseKey) {
if (!config.supabase.anonKey) {
throw new Error("Missing Supabase Key in configuration.");
}
supabaseClient = createClient(config.supabaseUrl, config.supabaseKey);
supabaseClient = createClient(config.supabase.url, config.supabase.anonKey);
}
return supabaseClient;
}
Expand Down
6 changes: 4 additions & 2 deletions apps/api/src/api/controllers/quote.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export const createQuote = async (
partnerId,
partnerName: publicKeyPartnerName,
rampType,
to
to,
userId: req.userId
});

res.status(httpStatus.CREATED).json(quote);
Expand Down Expand Up @@ -85,7 +86,8 @@ export const createBestQuote = async (
partnerId,
partnerName: publicKeyPartnerName,
rampType,
to
to,
userId: req.userId
});

res.status(httpStatus.CREATED).json(quote);
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/api/controllers/ramp.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export const registerRamp = async (req: Request, res: Response<RampProcess>, nex
const ramp = await rampService.registerRamp({
additionalData,
quoteId,
signingAccounts
signingAccounts,
userId: req.userId
});

res.status(httpStatus.CREATED).json(ramp);
Expand Down
76 changes: 76 additions & 0 deletions apps/api/src/api/middlewares/supabaseAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { NextFunction, Request, Response } from "express";
import logger from "../../config/logger";
import { SupabaseAuthService } from "../services/auth";

declare global {
namespace Express {
interface Request {
userId?: string;
}
}
}

/**
* Middleware to verify Supabase auth token and attach userId to request
*/
export async function requireAuth(req: Request, res: Response, next: NextFunction) {
try {
const authHeader = req.headers.authorization;

if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({
error: "Missing or invalid authorization header"
});
}

const token = authHeader.substring(7);
const result = await SupabaseAuthService.verifyToken(token);

if (!result.valid) {
return res.status(401).json({
error: "Invalid or expired token"
});
}

req.userId = result.user_id;
next();
} catch (error) {
console.error("Auth middleware error:", error);
return res.status(401).json({
error: "Authentication failed"
});
}
}

/**
* Optional auth - attaches userId if token present
*/
export async function optionalAuth(req: Request, res: Response, next: NextFunction) {
try {
const authHeader = req.headers.authorization;

if (authHeader?.startsWith("Bearer ")) {
const token = authHeader.substring(7);
const result = await SupabaseAuthService.verifyToken(token);

if (result.valid) {
req.userId = result.user_id;
}
}

next();
} catch (error) {
// Log truncated token for security - only show first/last few characters
const authHeader = req.headers.authorization;
const truncatedAuth = authHeader
? `${authHeader.substring(0, 15)}...${authHeader.substring(authHeader.length - 4)}`
: undefined;

logger.warn("optionalAuth middleware: authentication error", {
authorization: truncatedAuth,
error,
path: req.path
});
next();
}
}
12 changes: 12 additions & 0 deletions apps/api/src/api/routes/v1/auth.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Router } from "express";
import { AuthController } from "../../controllers/auth.controller";

const router = Router();

router.get("/check-email", AuthController.checkEmail);
router.post("/request-otp", AuthController.requestOTP);
router.post("/verify-otp", AuthController.verifyOTP);
router.post("/refresh", AuthController.refreshToken);
router.post("/verify", AuthController.verifyToken);

export default router;
Loading