Skip to content

Commit

Permalink
Merge pull request #69 from MinaFoundation/feature/wallet-user-and-ac…
Browse files Browse the repository at this point in the history
…count-linking

Feature/wallet user and account linking
  • Loading branch information
iluxonchik authored Dec 6, 2024
2 parents c4ffe67 + 734c431 commit dee2e3a
Show file tree
Hide file tree
Showing 29 changed files with 1,398 additions and 277 deletions.
43 changes: 33 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pgt-web-app",
"version": "0.1.8",
"version": "0.1.9",
"private": true,
"type": "module",
"scripts": {
Expand Down Expand Up @@ -44,6 +44,7 @@
"loglevel": "^1.9.2",
"loglevel-plugin-prefix": "^0.8.4",
"lucide-react": "^0.453.0",
"mina-signer": "^3.0.7",
"next": "15.0.1",
"next-pwa": "^5.6.0",
"next-themes": "^0.3.0",
Expand All @@ -63,7 +64,7 @@
"@types/react-dom": "^18",
"@types/uuid": "^10.0.0",
"esbuild": "^0.24.0",
"eslint": "9.15.0",
"eslint": "9.16.0",
"eslint-config-next": "15.0.1",
"postcss": "^8",
"prisma": "^5.22.0",
Expand Down
40 changes: 9 additions & 31 deletions src/app/api/auth/exchange/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { verifyToken, generateTokenPair } from "@/lib/auth/jwt";
import { verifyToken, generateTokenPair, setTokenCookies } from "@/lib/auth/jwt";
import { AppError } from "@/lib/errors";
import { ApiResponse } from "@/lib/api-response";
import logger from "@/logging";

export const runtime = "nodejs";
Expand All @@ -9,10 +10,7 @@ export async function POST(request: Request) {
const { initialToken } = await request.json();

if (!initialToken) {
return NextResponse.json(
{ error: "Initial token is required" },
{ status: 400 }
);
throw new AppError("Initial token is required", 400);
}

// Verify the initial token
Expand All @@ -23,32 +21,12 @@ export async function POST(request: Request) {
payload.authSource
);

// Create response with cookies
const response = NextResponse.json({ success: true });
// Create response and set cookies
const response = ApiResponse.success({ success: true });
return setTokenCookies(response, accessToken, refreshToken);

// Set cookies
response.cookies.set("access_token", accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 15 * 60, // 15 minutes
path: "/",
});

response.cookies.set("refresh_token", refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 7 * 24 * 60 * 60, // 7 days
path: "/",
});

return response;
} catch (error) {
logger.error("Token exchange error:", error);
return NextResponse.json(
{ error: "Invalid or expired token" },
{ status: 401 }
);
return ApiResponse.error(error);
}
}
}
40 changes: 11 additions & 29 deletions src/app/api/auth/refresh/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { verifyToken, generateTokenPair } from "@/lib/auth/jwt";
import { verifyToken, generateTokenPair, setTokenCookies } from "@/lib/auth/jwt";
import { AppError } from "@/lib/errors";
import { ApiResponse } from "@/lib/api-response";
import logger from "@/logging";

export const runtime = "nodejs";
Expand All @@ -11,42 +12,23 @@ export async function POST() {
const refreshToken = cookieStore.get("refresh_token")?.value;

if (!refreshToken) {
return NextResponse.json({ error: "No refresh token" }, { status: 401 });
throw new AppError("No refresh token", 401);
}

// Verify refresh token
const payload = await verifyToken(refreshToken);

// Generate new token pair
const { accessToken, refreshToken: newRefreshToken } =
await generateTokenPair(payload.authSource);

// Create response with new cookies
const response = NextResponse.json({ success: true });

// Set new cookies in response
response.cookies.set("access_token", accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 15 * 60, // 15 minutes
path: "/",
});
const { accessToken, refreshToken: newRefreshToken } = await generateTokenPair(
payload.authSource
);

response.cookies.set("refresh_token", newRefreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 7 * 24 * 60 * 60, // 7 days
path: "/",
});
// Create response and set cookies
const response = ApiResponse.success({ success: true });
return setTokenCookies(response, accessToken, newRefreshToken);

return response;
} catch (error) {
logger.error("Token refresh error:", error);
return NextResponse.json(
{ error: "Invalid refresh token" },
{ status: 401 }
);
return ApiResponse.error(error);
}
}
70 changes: 70 additions & 0 deletions src/app/api/auth/wallet/link/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { NextResponse } from "next/server";
import { verifyToken } from "@/lib/auth/jwt";
import prisma from "@/lib/prisma";
import logger from "@/logging";
import { AppError } from "@/lib/errors";
import { ApiResponse } from "@/lib/api-response";
import { deriveUserId } from "@/lib/user/derive";
import { UserService } from "@/services/UserService";

const userService = new UserService(prisma);

export const runtime = "nodejs";

interface LinkAccountRequest {
walletToken: string;
existingToken: string;
}

export async function POST(request: Request) {
try {
const body: LinkAccountRequest = await request.json();
const { walletToken, existingToken } = body;

if (!walletToken || !existingToken) {
throw new AppError("Missing required tokens", 400);
}

// Verify both tokens
const [walletPayload, existingPayload] = await Promise.all([
verifyToken(walletToken),
verifyToken(existingToken),
]);

if (walletPayload.authSource.type !== "wallet") {
throw new AppError("Invalid wallet token", 400);
}

// Derive user IDs from auth sources
const walletUserId = deriveUserId(walletPayload.authSource);
const existingUserId = deriveUserId(existingPayload.authSource);

// Check if linking is possible
const canLink = await userService.canLink(walletUserId, existingUserId);
if (!canLink) {
throw new AppError("Cannot link these accounts", 400);
}

// Link the accounts
const linked = await userService.linkAccounts(walletUserId, existingUserId);
if (!linked) {
throw new AppError("Failed to link accounts", 400);
}

return ApiResponse.success({
data: { message: "Accounts linked successfully" }
});

} catch (error) {
logger.error("Account linking error:", error);

if (error instanceof AppError) {
return ApiResponse.error(error);
}

return ApiResponse.error({
message: "Failed to link accounts",
statusCode: 500
});
}
}
Loading

0 comments on commit dee2e3a

Please sign in to comment.