Skip to content
This repository was archived by the owner on Sep 18, 2024. It is now read-only.

Commit

Permalink
Merge pull request #24 from multipletwigs/feature/POST-human-user
Browse files Browse the repository at this point in the history
feature: Human User Creation Endpoint
  • Loading branch information
GuiBibeau authored Feb 16, 2024
2 parents 054001f + a97e48e commit 13f67e9
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 1 deletion.
Binary file modified bun.lockb
Binary file not shown.
21 changes: 21 additions & 0 deletions src/__tests__/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
createApiToken,
createSuperAdmin,
} from "@/core/application/controllers/userController";
import { createHumanUser } from "@/core/application/services/userService";
import { ulid } from "ulid";

export async function createSuperAdminForTesting(): Promise<string | null> {
const userData = {
Expand All @@ -20,3 +22,22 @@ export async function createSuperAdminForTesting(): Promise<string | null> {

return apiKey;
}

export async function createHumanUserForTesting(): Promise<string | null> {
const userData = {
name: "Sprout Tester",
email: "[email protected]"
};

const humanUserId = await createHumanUser(userData);
if (!humanUserId) {
throw new Error("Failed to create human user");
}

const apiKey = await createApiToken(humanUserId);
if (!apiKey) {
throw new Error("Failed to create API key for human user");
}

return apiKey;
}
24 changes: 24 additions & 0 deletions src/core/application/controllers/user/returnValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const UNAUTHORIZED_INVALID_TOKEN = {
message: "Unauthorized: Invalid token",
code: 401,
}

export const UNAUTHORIZED_NO_PERMISSION_CREATE = {
message: "Unauthorized: User does not have permission to create users",
code: 403,
};

export const UNAUTHORIZED_NO_PERMISSION_DELETE = {
message: "Unauthorized: User does not have permission to delete users",
code: 403,
}

export const USER_CREATED_SUCCESSFULLY = {
message: "User created successfully",
code: 201,
}

export const UNSUCCESSFUL_USER_CREATION = {
message: "User creation not successful. Something went wrong.",
code: 500,
}
133 changes: 133 additions & 0 deletions src/core/application/controllers/user/userController.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { createHumanUserForTesting, createSuperAdminForTesting } from "@/__tests__/utils";
import { app } from "@/index";
import { test, expect, describe, beforeAll } from "bun:test";
import { getUser } from "../../services/userService";
import { UNAUTHORIZED_MISSING_TOKEN } from "../../ports/returnValues";
import { UNAUTHORIZED_NO_PERMISSION_CREATE } from "./returnValues";
import { getThread } from "../../services/threadService";
import { parseToken } from "../../services/tokenService";
import { Thread } from "@/core/domain/thread";

describe.only("userController", async () => {
let superAdminToken: string | null;
let humanUserToken: string | null;

beforeAll(async () => {
superAdminToken = await createSuperAdminForTesting();
humanUserToken = await createHumanUserForTesting();
});

test("allows creating a user and the user is saved in the database", async () => {
const request = new Request("http://localhost:8080/users", {
headers: {
authorization: `Bearer ${superAdminToken}`,
"Content-Type": "application/json"
},
method: "POST",
body: JSON.stringify({
name: "Mr. Sprout",
email: "[email protected]"
})
});

const response = await app.handle(request);
const responseJson: any = await response.json();

expect(responseJson).toHaveProperty("token");
const apiToken = responseJson.token;
expect(apiToken).not.toBeNull();

// From api token we can get the user id
const parsedToken = await parseToken(apiToken);

if (parsedToken) {
const user = await getUser(parsedToken.userId);
expect(user).not.toBeNull();
expect(user?.name).toEqual("Mr. Sprout");
expect(user?.email).toEqual("[email protected]");
}
});

test("prevents from creating a user if there is no api token present", async () => {
const request = new Request("http://localhost:8080/users", {
headers: {
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
name: "Mr. Sprout",
email: "[email protected]"
})
});

const response: any = await app.handle(request).then((response) => response.json())
expect(response.message).toBe(UNAUTHORIZED_MISSING_TOKEN.message);
});

test("prevents from creating a user if the user does not have CREATE_USER permission", async () => {
const request = new Request("http://localhost:8080/users", {
headers: {
authorization: `Bearer ${humanUserToken}`,
"Content-Type": "application/json"
},
method: "POST",
body: JSON.stringify({
name: "Jr. Sprout",
email: "[email protected]"
})
});

const response: any = await app.handle(request);
const responseJson = await response.json();
expect(responseJson.message).toBe(UNAUTHORIZED_NO_PERMISSION_CREATE.message);
});

test("allows a human user to create a thread", async () => {
const request = new Request("http://localhost:8080/thread", {
headers: {
authorization: `Bearer ${humanUserToken}`,
"Content-type": "application/json",
},
method: "POST",
});

const response = await app.handle(request);
const responseJson: any = await response.json();

expect(responseJson).toHaveProperty("id");

const id = responseJson.id;

const thread = await getThread(id);

expect(thread).not.toBeNull();
expect(thread?.id).toEqual(id);
})

test("allows a human user to add messages to a thread they created", async () => {
// Creating a thread for the test
const createThreadResponse = await app.handle(new Request("http://localhost:8080/thread", {
headers: {
authorization: `Bearer ${humanUserToken}`,
"Content-type": "application/json",
},
method: "POST",
}));
const thread: Thread = await createThreadResponse.json() as Thread;
expect(thread).toHaveProperty("id");

// Adding a message to the thread
const response = await app.handle(new Request(`http://localhost:8080/thread/${thread.id}/message`, {
headers: {
authorization: `Bearer ${humanUserToken}`,
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify({ message: "Human user from userController test!" })
}));
const messageResponse = await response.json() as any;

expect(messageResponse).toHaveProperty("content");
expect(messageResponse.content).toBe("Human user from userController test!");
});
})
66 changes: 66 additions & 0 deletions src/core/application/controllers/user/userController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import Elysia, { t } from "elysia";
import { UNAUTHORIZED_MISSING_TOKEN } from "../../ports/returnValues";
import { createToken, getTokenPermissions } from "@/core/application/services/tokenService";
import { UNAUTHORIZED_INVALID_TOKEN } from "../thread/returnValues";
import { UNAUTHORIZED_NO_PERMISSION_CREATE, UNSUCCESSFUL_USER_CREATION, USER_CREATED_SUCCESSFULLY } from "./returnValues";
import { createHumanUser } from "../../services/userService";

type UserDecorator = {
request: {
bearer: string | undefined;
};
store: {};
derive: {};
resolve: {};
};

export const users = new Elysia<"/users", UserDecorator>();

/**
* Guard for the user controller.
* @param {string | undefined} bearer - The bearer token.
* @returns {Promise<Error | undefined>} - Returns an error if the token is missing, invalid or does not have CREATE_USER.
*/
users.guard({
beforeHandle: [async ({ bearer, set }) => {
if (!bearer) {
set.status = UNAUTHORIZED_MISSING_TOKEN.code;
return UNAUTHORIZED_MISSING_TOKEN;
}

// Check that the token is valid and has the required permission.
const permissions = await getTokenPermissions(bearer);
if (!permissions) {
set.status = UNAUTHORIZED_INVALID_TOKEN.code;
return UNAUTHORIZED_INVALID_TOKEN;
}

if (!permissions.some(p => ["create_user", "*"].includes(p.key))) {
set.status = UNAUTHORIZED_NO_PERMISSION_CREATE.code;
return UNAUTHORIZED_NO_PERMISSION_CREATE;
}
}],
})


users.post("/users", async ({ set, body }) => {
// Human user uses the User role
const humanUserId = await createHumanUser(body);
if (humanUserId) {
const humanUserToken = await createToken(humanUserId);

set.status = USER_CREATED_SUCCESSFULLY.code;
return {
...USER_CREATED_SUCCESSFULLY,
token: humanUserToken,
};
}

set.status = UNSUCCESSFUL_USER_CREATION.code;
return UNSUCCESSFUL_USER_CREATION;
}, {
body: t.Object({
email: t.String(),
name: t.String(),
}),
});
29 changes: 29 additions & 0 deletions src/core/application/services/userService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// userService.js

import { Role, getRolePermissions } from "@/core/domain/roles";
import { HumanUserBody, User } from "@/core/domain/user";
import { redis } from "@/infrastructure/adaptaters/redisAdapter";
import { ulid } from "ulid";

/**
* Function to create a new user
Expand Down Expand Up @@ -119,3 +121,30 @@ export const getUserPermissions = async (userId: string): Promise<string[]> => {
return [];
}
};

export const getUser = async (userId: string): Promise<User | null> => {
const userData = await redis.hget("users", userId);
if (!userData) return null

return JSON.parse(userData)
}

/**
* Creates a new human user with a unique identifier and assigns them a 'user' role.
* If the user cannot be created or the role cannot be assigned, the function returns `null`.
*
* @param {User} userData - An object containing the data of the user to be created.
* @returns {Promise<string|null>} A promise that resolves to the user ID of the newly created user,
* or `null` if the user could not be created or the role could not be assigned.
*/
export const createHumanUser = async (userData: HumanUserBody): Promise<string | null> => {
const userId = ulid();

const userCreated = await createUser(userId, userData);
if (!userCreated) return null

const roleAssigned = await assignRole(userId, "user");
if (!roleAssigned) return null

return userId
}
4 changes: 3 additions & 1 deletion src/core/domain/user.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export type User = {
id: string;
name: string;
description: string;
email: string;
};

export type HumanUserBody = Omit<User, 'id'>;
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { bearer } from "@elysiajs/bearer";

import { threads } from "@/core/application/controllers/thread/threadController";
import { assistants } from "@/core/application/controllers/assistant/assistantController";
import { users } from "./core/application/controllers/user/userController";

export const name = "Sprout";

Expand All @@ -20,6 +21,7 @@ export const app = new Elysia()
.use(bearer())
.use(assistants)
.use(threads)
.use(users)
.get("/", healthCheck)
.listen(process.env.PORT || 8080);

Expand Down
1 change: 1 addition & 0 deletions src/infrastructure/config/redisConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export const redisConfig = {
? { password: process.env.REDIS_PASSWORD }
: {}), // Redis password
db: process.env.REDIS_DB ? parseInt(process.env.REDIS_DB) : 0, // Redis DB
tls: {}
};

0 comments on commit 13f67e9

Please sign in to comment.