Skip to content

Commit

Permalink
consolidate session endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
0age committed Dec 5, 2024
1 parent 9f6a6b9 commit 0b99fd7
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 48 deletions.
145 changes: 145 additions & 0 deletions src/__tests__/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,151 @@ describe('API Routes', () => {
});
});

describe('Session Management', () => {
let sessionId: string;
let address: string;

beforeEach(async () => {
address = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
// First get a session request
const sessionResponse = await server.inject({
method: 'GET',
url: `/session/1/${address}`,
});

expect(sessionResponse.statusCode).toBe(200);
const sessionRequest = JSON.parse(sessionResponse.payload);

// Normalize timestamps to match database precision
const payload = {
...sessionRequest.session,
issuedAt: new Date(sessionRequest.session.issuedAt).toISOString(),
expirationTime: new Date(
sessionRequest.session.expirationTime
).toISOString(),
};

// Create a valid session
const signature = await generateSignature(payload);
const response = await server.inject({
method: 'POST',
url: '/session',
payload: {
payload,
signature,
},
});

const result = JSON.parse(response.payload);
expect(response.statusCode).toBe(200);
expect(result.session?.id).toBeDefined();
sessionId = result.session.id;
});

describe('GET /session', () => {
it('should verify valid session', async () => {
const response = await server.inject({
method: 'GET',
url: '/session',
headers: {
'x-session-id': sessionId,
},
});

expect(response.statusCode).toBe(200);
const result = JSON.parse(response.payload);
expect(result.session).toBeDefined();
expect(result.session.id).toBe(sessionId);
expect(result.session.address).toBe(address);
expect(result.session.expiresAt).toBeDefined();
});

it('should reject invalid session ID', async () => {
const response = await server.inject({
method: 'GET',
url: '/session',
headers: {
'x-session-id': 'invalid-session-id',
},
});

expect(response.statusCode).toBe(401);
const result = JSON.parse(response.payload);
expect(result.error).toBeDefined();
});

it('should reject missing session ID', async () => {
const response = await server.inject({
method: 'GET',
url: '/session',
});

expect(response.statusCode).toBe(401);
const result = JSON.parse(response.payload);
expect(result.error).toBe('Session ID required');
});
});

describe('DELETE /session', () => {
it('should delete valid session', async () => {
// First verify session exists
const verifyResponse = await server.inject({
method: 'GET',
url: '/session',
headers: {
'x-session-id': sessionId,
},
});
expect(verifyResponse.statusCode).toBe(200);

// Delete session
const deleteResponse = await server.inject({
method: 'DELETE',
url: '/session',
headers: {
'x-session-id': sessionId,
},
});
expect(deleteResponse.statusCode).toBe(200);

// Verify session is gone
const finalResponse = await server.inject({
method: 'GET',
url: '/session',
headers: {
'x-session-id': sessionId,
},
});
expect(finalResponse.statusCode).toBe(401);
});

it('should reject deleting invalid session', async () => {
const response = await server.inject({
method: 'DELETE',
url: '/session',
headers: {
'x-session-id': 'invalid-session-id',
},
});

expect(response.statusCode).toBe(401);
const result = JSON.parse(response.payload);
expect(result.error).toBeDefined();
});

it('should reject deleting without session ID', async () => {
const response = await server.inject({
method: 'DELETE',
url: '/session',
});

expect(response.statusCode).toBe(401);
const result = JSON.parse(response.payload);
expect(result.error).toBe('Session ID required');
});
});
});

describe('Protected Routes', () => {
let sessionId: string;

Expand Down
18 changes: 10 additions & 8 deletions src/__tests__/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,21 +143,24 @@ describe('Session Management', () => {
it('should verify valid session', async () => {
const response = await server.inject({
method: 'GET',
url: '/session/verify',
url: '/session',
headers: {
'x-session-id': sessionId,
},
});

expect(response.statusCode).toBe(200);
const result = JSON.parse(response.payload);
expect(result).toHaveProperty('address', getFreshValidPayload().address);
expect(result.session).toBeDefined();
expect(result.session.address).toBe(getFreshValidPayload().address);
expect(result.session.id).toBe(sessionId);
expect(result.session.expiresAt).toBeDefined();
});

it('should reject invalid session ID', async () => {
const response = await server.inject({
method: 'GET',
url: '/session/verify',
url: '/session',
headers: {
'x-session-id': 'invalid-session-id',
},
Expand All @@ -169,16 +172,15 @@ describe('Session Management', () => {
});

it('should reject expired session', async () => {
// Set session to expired in database
const expiredTimestamp = new Date(Date.now() - 3600000); // 1 hour ago
// First create an expired session
await server.db.query(
'UPDATE sessions SET expires_at = $1 WHERE id = $2',
[expiredTimestamp.toISOString(), sessionId]
"UPDATE sessions SET expires_at = CURRENT_TIMESTAMP - interval '1 hour' WHERE id = $1",
[sessionId]
);

const response = await server.inject({
method: 'GET',
url: '/session/verify',
url: '/session',
headers: {
'x-session-id': sessionId,
},
Expand Down
53 changes: 13 additions & 40 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,16 +253,19 @@ export async function setupRoutes(server: FastifyInstance): Promise<void> {
return reply.code(401).send({ error: 'Session ID required' });
}

// Query the session
// Verify and get session
await verifySession(server, sessionId);

// Get full session details
const result = await server.db.query<{
id: string;
address: string;
expires_at: string;
}>(
'SELECT id, address, expires_at FROM sessions WHERE id = $1 AND expires_at > CURRENT_TIMESTAMP',
[sessionId]
);
}>('SELECT id, address, expires_at FROM sessions WHERE id = $1', [
sessionId,
]);

// This should never happen since verifySession would throw, but TypeScript doesn't know that
if (!result.rows || result.rows.length === 0) {
return reply
.code(404)
Expand All @@ -278,45 +281,15 @@ export async function setupRoutes(server: FastifyInstance): Promise<void> {
},
};
} catch (error) {
server.log.error('Failed to get session:', error);
return reply.code(500).send({
error: 'Failed to get session status',
});
}
}
);

// Verify session
server.get(
'/session/verify',
async (
request: FastifyRequest<{ Headers: { 'x-session-id'?: string } }>,
reply: FastifyReply
): Promise<{ address: string } | { error: string }> => {
const sessionId = request.headers['x-session-id'];
if (!sessionId || Array.isArray(sessionId)) {
reply.code(401);
return { error: 'Session ID required' };
}

try {
await verifySession(server, sessionId);
const result = await server.db.query<{ address: string }>(
'SELECT address FROM sessions WHERE id = $1',
[sessionId]
);
return { address: result.rows[0].address };
} catch (err) {
server.log.error({
msg: 'Session verification failed',
err: err instanceof Error ? err.message : String(err),
sessionId,
err: error instanceof Error ? error.message : String(error),
sessionId: request.headers['x-session-id'],
path: request.url,
});
reply.code(401);
return {
error: err instanceof Error ? err.message : 'Invalid session',
};
return reply.code(401).send({
error: error instanceof Error ? error.message : 'Invalid session',
});
}
}
);
Expand Down

0 comments on commit 0b99fd7

Please sign in to comment.