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
17 changes: 16 additions & 1 deletion app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,23 @@ export async function POST(request: NextRequest) {
);
}

// Upsert user in database (best-effort — don't fail login if DB is unavailable)
// Check if user exists and is deactivated
let user;
try {
user = await prisma.user.findUnique({
where: { stellar_address: address },
select: { deletedAt: true },
});

// Prevent login for deactivated users
if (user?.deletedAt) {
return NextResponse.json(
{ error: 'USER_DEACTIVATED', message: 'Account has been deactivated' },
{ status: 410 }
);
}

// Upsert user in database (best-effort — don't fail login if DB is unavailable)
await prisma.user.upsert({
where: { stellar_address: address },
update: {},
Expand Down
51 changes: 51 additions & 0 deletions app/api/user/deactivate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { NextResponse } from 'next/server';
import { requireAuth } from '@/lib/session';
import { prisma } from '@/lib/prisma';

/**
* POST /api/user/deactivate
* Soft-delete (deactivate) the current user account
* Sets deletedAt timestamp without removing data
*/
export async function POST() {
try {
const { address } = await requireAuth();

const user = await prisma.user.findUnique({
where: { stellar_address: address },
});

if (!user) {
return NextResponse.json(
{ error: 'USER_NOT_FOUND' },
{ status: 404 }
);
}

// Check if already deactivated
if (user.deletedAt) {
return NextResponse.json(
{ error: 'USER_ALREADY_DEACTIVATED' },
{ status: 400 }
);
}

// Soft delete by setting deletedAt timestamp
await prisma.user.update({
where: { id: user.id },
data: { deletedAt: new Date() },
});

return NextResponse.json({
message: 'Account deactivated successfully',
deactivatedAt: new Date().toISOString(),
});
} catch (error) {
if (error instanceof Response) return error;

return NextResponse.json(
{ error: 'INTERNAL_ERROR' },
{ status: 500 }
);
}
}
16 changes: 16 additions & 0 deletions app/api/user/preferences/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ export async function PATCH(request: NextRequest) {
);
}

// Return 410 Gone if user is deactivated
if (user.deletedAt) {
return NextResponse.json(
{ error: 'USER_DEACTIVATED', message: 'User account has been deactivated' },
{ status: 410 }
);
}

const updated = await prisma.userPreference.update({
where: { userId: user.id },
data: {
Expand Down Expand Up @@ -66,6 +74,14 @@ export async function GET() {
);
}

// Return 410 Gone if user is deactivated
if (user.deletedAt) {
return NextResponse.json(
{ error: 'USER_DEACTIVATED', message: 'User account has been deactivated' },
{ status: 410 }
);
}

return NextResponse.json(user.preferences);
} catch (error) {
if (error instanceof Response) return error;
Expand Down
8 changes: 8 additions & 0 deletions app/api/user/profile/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ export async function GET() {
);
}

// Return 410 Gone if user is deactivated
if (user.deletedAt) {
return NextResponse.json(
{ error: 'USER_DEACTIVATED', message: 'User account has been deactivated' },
{ status: 410 }
);
}

return NextResponse.json({
address: user.stellar_address,
preferences: {
Expand Down
22 changes: 22 additions & 0 deletions lib/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ export async function getSessionWithRefresh(): Promise<SessionData | null> {
* Require authentication or throw 401 Response
* Validates session and returns authenticated user data on success
* Throws Response with 401 status if session is invalid or expired
* Throws Response with 410 status if user account is deactivated
* @returns Object containing the authenticated user's wallet address
* @throws Response with 401 status and appropriate error message
*/
Expand Down Expand Up @@ -336,6 +337,27 @@ export async function requireAuth(): Promise<{ address: string }> {
);
}

// Check if user account is deactivated (soft-deleted)
// Import prisma dynamically to avoid circular dependencies
const { prisma } = await import('@/lib/prisma');
const user = await prisma.user.findUnique({
where: { stellar_address: data.address },
select: { deletedAt: true },
});

if (user?.deletedAt) {
throw new Response(
JSON.stringify({ error: 'USER_DEACTIVATED', message: 'User account has been deactivated' }),
{
status: 410,
headers: {
'Content-Type': 'application/json',
'Set-Cookie': clearSessionCookie()
}
}
);
}

return { address: data.address };
} catch (error) {
// If error is already a Response, re-throw it
Expand Down
114 changes: 114 additions & 0 deletions lib/user-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* User helper utilities for querying and managing users
* Includes soft-delete aware query helpers
*/

import { prisma } from '@/lib/prisma';

/**
* Get active user by stellar address (excludes deactivated users)
* @param address - Stellar wallet address
* @param includePreferences - Whether to include user preferences
* @returns User object or null if not found or deactivated
*/
export async function getActiveUser(
address: string,
includePreferences: boolean = false
) {
return prisma.user.findFirst({
where: {
stellar_address: address,
deletedAt: null, // Only active users
},
include: includePreferences ? { preferences: true } : undefined,
});
}

/**
* Get all active users (excludes deactivated users)
* @param includePreferences - Whether to include user preferences
* @returns Array of active users
*/
export async function getActiveUsers(includePreferences: boolean = false) {
return prisma.user.findMany({
where: {
deletedAt: null, // Only active users
},
include: includePreferences ? { preferences: true } : undefined,
});
}

/**
* Count active users (excludes deactivated users)
* @returns Count of active users
*/
export async function countActiveUsers() {
return prisma.user.count({
where: {
deletedAt: null, // Only active users
},
});
}

/**
* Check if a user is deactivated
* @param address - Stellar wallet address
* @returns true if user is deactivated, false otherwise
*/
export async function isUserDeactivated(address: string): Promise<boolean> {
const user = await prisma.user.findUnique({
where: { stellar_address: address },
select: { deletedAt: true },
});
return user?.deletedAt !== null && user?.deletedAt !== undefined;
}

/**
* Get deactivated users that are past retention period
* Used for automated purge jobs
* @param retentionDays - Number of days to retain deactivated users (default: 90)
* @returns Array of users eligible for purge
*/
export async function getUsersEligibleForPurge(retentionDays: number = 90) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);

return prisma.user.findMany({
where: {
deletedAt: {
not: null,
lt: cutoffDate, // Deactivated before cutoff date
},
},
});
}

/**
* Admin function: Get user including deactivated status
* Should only be used in admin contexts
* @param address - Stellar wallet address
* @param includePreferences - Whether to include user preferences
* @returns User object or null if not found
*/
export async function getUserIncludingDeactivated(
address: string,
includePreferences: boolean = false
) {
return prisma.user.findUnique({
where: { stellar_address: address },
include: includePreferences ? { preferences: true } : undefined,
});
}

/**
* Admin function: Reactivate a deactivated user
* Sets deletedAt to null
* @param address - Stellar wallet address
* @returns Updated user object
*/
export async function reactivateUser(address: string) {
return prisma.user.update({
where: { stellar_address: address },
data: { deletedAt: null },
});
}
Loading
Loading