alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
434 lines (355 loc) • 12.4 kB
text/typescript
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();
});
});