UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

434 lines (355 loc) 12.4 kB
import { Alepha } from "alepha"; import { ApiKeyService } from "alepha/api/keys"; import { DateTimeProvider } from "alepha/datetime"; import { AlephaEmail } from "alepha/email"; import { AlephaOrmPostgres } from "alepha/orm/postgres"; import { AlephaSecurity } from "alepha/security"; import { AlephaServer, ForbiddenError } from "alepha/server"; import { describe, it } from "vitest"; import { $realm, AlephaApiUsers, SessionService } from "../index.ts"; const setup = async () => { const alepha = Alepha.create({ env: { LOG_LEVEL: "error" }, }); alepha.with(AlephaOrmPostgres); alepha.with(AlephaServer); alepha.with(AlephaSecurity); alepha.with(AlephaEmail); alepha.with(AlephaApiUsers); // Create a realm with API keys enabled class TestApp { realm = $realm({ features: { apiKeys: true }, }); } alepha.inject(TestApp); await alepha.start(); const sessionService = alepha.inject(SessionService); const apiKeyService = alepha.inject(ApiKeyService); const dateTimeProvider = alepha.inject(DateTimeProvider); return { alepha, sessionService, apiKeyService, dateTimeProvider, }; }; describe("alepha/api/users - API Keys Integration", () => { it("should create an API key for a user", async ({ expect }) => { const { sessionService, apiKeyService } = await setup(); // Create a user const user = await sessionService.users().create({ username: "testuser", email: "test@example.com", roles: ["user"], }); // Create an API key const { apiKey, token } = await apiKeyService.create({ userId: user.id, name: "My API Key", roles: ["user"], }); expect(apiKey.name).toBe("My API Key"); expect(apiKey.userId).toBe(user.id); expect(apiKey.roles).toEqual(["user"]); expect(token).toMatch(/^ak_/); }); it("should validate API key and return user info", async ({ expect }) => { const { sessionService, apiKeyService } = await setup(); // Create a user const user = await sessionService.users().create({ username: "testuser", email: "test@example.com", roles: ["user"], }); // Create an API key const { token } = await apiKeyService.create({ userId: user.id, name: "My API Key", roles: ["user"], }); // Validate the API key const userInfo = await apiKeyService.validate(token); expect(userInfo).not.toBeNull(); expect(userInfo?.id).toBe(user.id); expect(userInfo?.roles).toEqual(["user"]); }); it("should resolve user from API key in query param via resolver", async ({ expect, }) => { const { sessionService, apiKeyService } = await setup(); // Create a user const user = await sessionService.users().create({ username: "testuser", email: "test@example.com", roles: ["user"], }); // Create an API key const { token } = await apiKeyService.create({ userId: user.id, name: "My API Key", roles: ["user"], }); // Create resolver and test with query param const resolver = apiKeyService.createResolver(); const userInfo = await resolver.onRequest({ url: new URL(`http://localhost/profile?api_key=${token}`), headers: {}, } as any); expect(userInfo).not.toBeNull(); expect(userInfo?.id).toBe(user.id); expect(userInfo?.roles).toEqual(["user"]); }); it("should resolve user from API key in Bearer header via resolver", async ({ expect, }) => { const { sessionService, apiKeyService } = await setup(); // Create a user const user = await sessionService.users().create({ username: "testuser", email: "test@example.com", roles: ["user"], }); // Create an API key const { token } = await apiKeyService.create({ userId: user.id, name: "My API Key", roles: ["user"], }); // Create resolver and test with Bearer header const resolver = apiKeyService.createResolver(); const userInfo = await resolver.onRequest({ url: new URL("http://localhost/profile"), headers: { authorization: `Bearer ${token}` }, } as any); expect(userInfo).not.toBeNull(); expect(userInfo?.id).toBe(user.id); expect(userInfo?.roles).toEqual(["user"]); }); it("should not resolve JWT tokens as API keys", async ({ expect }) => { const { apiKeyService } = await setup(); // Create resolver and test with a JWT-like token (contains dots) const resolver = apiKeyService.createResolver(); const userInfo = await resolver.onRequest({ url: new URL("http://localhost/profile"), headers: { authorization: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test.signature", }, } as any); // Should return null because JWT tokens should be handled by JWT resolver expect(userInfo).toBeNull(); }); it("should update lastUsedAt and usageCount on validate", async ({ expect, }) => { const { sessionService, apiKeyService } = await setup(); const user = await sessionService.users().create({ username: "testuser", email: "test@example.com", roles: ["user"], }); const { apiKey, token } = await apiKeyService.create({ userId: user.id, name: "My API Key", roles: ["user"], }); expect(apiKey.usageCount).toBe(0); expect(apiKey.lastUsedAt).toBeUndefined(); const info = await apiKeyService.validate(token); expect(info).not.toBeNull(); // updateUsage is fire-and-forget — wait for the next microtask flush await new Promise((r) => setTimeout(r, 50)); const after = await (apiKeyService as any).repo.getById(apiKey.id); expect(after.lastUsedAt).toBeDefined(); expect(after.usageCount).toBe(1); }); it("should reject revoked API key", async ({ expect }) => { const { sessionService, apiKeyService } = await setup(); // Create a user const user = await sessionService.users().create({ username: "testuser", email: "test@example.com", roles: ["user"], }); // Create an API key const { apiKey, token } = await apiKeyService.create({ userId: user.id, name: "My API Key", roles: ["user"], }); // Verify key is valid const beforeRevoke = await apiKeyService.validate(token); expect(beforeRevoke).not.toBeNull(); // Revoke the key await apiKeyService.revoke(apiKey.id, user.id); // Verify key is now invalid const afterRevoke = await apiKeyService.validate(token); expect(afterRevoke).toBeNull(); }); it("should reject expired API key", async ({ expect }) => { const { sessionService, apiKeyService, dateTimeProvider } = await setup(); // Create a user const user = await sessionService.users().create({ username: "testuser", email: "test@example.com", roles: ["user"], }); // Create an API key that expires in 1 hour const expiresAt = dateTimeProvider.now().add(1, "hour").toDate(); const { token } = await apiKeyService.create({ userId: user.id, name: "Expiring Key", roles: ["user"], expiresAt, }); // Verify key is valid const beforeExpiry = await apiKeyService.validate(token); expect(beforeExpiry).not.toBeNull(); // Travel forward in time past expiry dateTimeProvider.travel(2, "hours"); // Verify key is now invalid const afterExpiry = await apiKeyService.validate(token); expect(afterExpiry).toBeNull(); }); it("should reject invalid API key", async ({ expect }) => { await setup(); const { apiKeyService } = await setup(); // Try to validate an invalid token const result = await apiKeyService.validate("ak_invalid_token_12345678"); expect(result).toBeNull(); }); it("should list user's API keys", async ({ expect }) => { const { sessionService, apiKeyService } = await setup(); // Create a user const user = await sessionService.users().create({ username: "testuser", email: "test@example.com", roles: ["user"], }); // Create multiple API keys await apiKeyService.create({ userId: user.id, name: "Key 1", roles: ["user"], }); await apiKeyService.create({ userId: user.id, name: "Key 2", roles: ["user"], }); // List keys const keys = await apiKeyService.list(user.id); expect(keys).toHaveLength(2); expect(keys.map((k) => k.name).sort()).toEqual(["Key 1", "Key 2"]); }); it("should not allow revoking another user's API key", async ({ expect }) => { const { sessionService, apiKeyService } = await setup(); // Create two users const user1 = await sessionService.users().create({ username: "user1", email: "user1@example.com", roles: ["user"], }); const user2 = await sessionService.users().create({ username: "user2", email: "user2@example.com", roles: ["user"], }); // Create an API key for user1 const { apiKey } = await apiKeyService.create({ userId: user1.id, name: "User1 Key", roles: ["user"], }); // Try to revoke user1's key as user2 await expect( apiKeyService.revoke(apiKey.id, user2.id), ).rejects.toThrowError(ForbiddenError); }); it("should create API key with description", async ({ expect }) => { const { sessionService, apiKeyService } = await setup(); // Create a user const user = await sessionService.users().create({ username: "testuser", email: "test@example.com", roles: ["user", "admin"], }); // Create API key with description const { apiKey, token } = await apiKeyService.create({ userId: user.id, name: "My Key", description: "Used for CI/CD pipeline", roles: ["user", "admin"], }); expect(apiKey.name).toBe("My Key"); expect(apiKey.description).toBe("Used for CI/CD pipeline"); expect(apiKey.roles).toEqual(["user", "admin"]); expect(token).toMatch(/^ak_/); // Validate works const userInfo = await apiKeyService.validate(token); expect(userInfo?.id).toBe(user.id); expect(userInfo?.roles).toEqual(["user", "admin"]); }); it("should support API key with admin role", async ({ expect }) => { const { sessionService, apiKeyService } = await setup(); // Create an admin user const adminUser = await sessionService.users().create({ username: "admin", email: "admin@example.com", roles: ["admin"], }); // Create an API key with admin role const { token } = await apiKeyService.create({ userId: adminUser.id, name: "Admin API Key", roles: ["admin"], }); // Validate API key returns admin role const userInfo = await apiKeyService.validate(token); expect(userInfo).not.toBeNull(); expect(userInfo?.id).toBe(adminUser.id); expect(userInfo?.roles).toEqual(["admin"]); }); it("should not revoke already revoked keys twice", async ({ expect }) => { const { sessionService, apiKeyService } = await setup(); // Create a user const user = await sessionService.users().create({ username: "testuser", email: "test@example.com", roles: ["user"], }); // Create and revoke an API key const { apiKey, token } = await apiKeyService.create({ userId: user.id, name: "My Key", roles: ["user"], }); await apiKeyService.revoke(apiKey.id, user.id); // Verify key is revoked const result = await apiKeyService.validate(token); expect(result).toBeNull(); // Admin revoke should not fail on already revoked key await apiKeyService.revokeByAdmin(apiKey.id); }); it("should create API key with custom prefix", async ({ expect }) => { const { sessionService, apiKeyService } = await setup(); // Create a user const user = await sessionService.users().create({ username: "testuser", email: "test@example.com", roles: ["user"], }); // Create an API key with custom prefix const { token } = await apiKeyService.create({ userId: user.id, name: "Custom Key", roles: ["user"], prefix: "prod", }); expect(token).toMatch(/^prod_/); // Should still validate const userInfo = await apiKeyService.validate(token); expect(userInfo).not.toBeNull(); }); });