Skip to content

Commit

Permalink
Add an endpoint to retrieve user information
Browse files Browse the repository at this point in the history
  • Loading branch information
Dlurak committed Mar 29, 2024
1 parent 7e8f108 commit 4ff36d4
Show file tree
Hide file tree
Showing 12 changed files with 218 additions and 8 deletions.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"edgedb": "bunx @edgedb/generate edgeql-js"
},
"dependencies": {
"@elysiajs/bearer": "^1.0.2",
"@elysiajs/swagger": "^1.0.3",
"edgedb": "^1.4.1",
"elysia": "latest",
Expand Down
5 changes: 5 additions & 0 deletions src/constants/responses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { responseBuilder } from "utils/response";

export const UNAUTHORIZED = responseBuilder("error", {
error: "Unauthorized",
});
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Elysia } from "elysia";
import { accessTokenRouter } from "routes/auth/accessToken";
import { refreshTokenRouter } from "routes/auth/refreshToken";
import { registerRouter } from "routes/auth/register";
import { userInfoRouter } from "routes/user/info";
import { edgedb } from "../dbschema/edgeql-js/imports";

export const client = edgedb.createClient();
Expand All @@ -28,6 +29,7 @@ const app = new Elysia()
tags: [
{ name: "App", description: "General app information" },
{ name: "Auth", description: "Authentication endpoints" },
{ name: "User", description: "User information endpoints" },
],
},
}),
Expand All @@ -48,6 +50,7 @@ const app = new Elysia()
.group("/auth", (app) =>
app.use(registerRouter).use(refreshTokenRouter).use(accessTokenRouter),
)
.group("/user", (app) => app.use(userInfoRouter))
.listen(3000);

console.log(
Expand Down
4 changes: 4 additions & 0 deletions src/plugins/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Plugins

All plugins are located in this directory.
In Elysia-JS a plugin is middleware.
39 changes: 39 additions & 0 deletions src/plugins/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { bearer } from "@elysiajs/bearer";
import Elysia from "elysia";
import { verifyToken } from "utils/auth/jwt";

type IsAuthorized = {
isAuthorized: true;
username: string;
createdBy: "login" | "refresh";
};
type IsNotAuthorized = {
isAuthorized: false;
};

type Auth = IsAuthorized | IsNotAuthorized;

/**
* Middleware to check if the user is authorized
*/
export const auth = new Elysia({
name: "auth-plugin",
})
.use(bearer())
.derive({ as: "global" }, ({ bearer }) => ({
get auth(): Auth {
const unauthorizedRes = { isAuthorized: false } as const;

if (!bearer) return unauthorizedRes;

const { payload, isValid } = verifyToken(bearer);
if (!isValid) return unauthorizedRes;
if (!(payload.type === "access")) return unauthorizedRes;

return {
isAuthorized: true,
username: payload.username,
createdBy: payload.createdBy,
};
},
}));
19 changes: 11 additions & 8 deletions src/routes/auth/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@ export const registerRouter = new Elysia({ prefix: "/register" })

return creationQuery
.run(client)
.then((data) => ({
status: "success",
message: "User created successfully",
data: {
id: data.id,
usernmae: body.username,
},
}))
.then((data) => {
set.status = httpStatus.HTTP_201_CREATED;
return {
status: "success",
message: "User created successfully",
data: {
id: data.id,
usernmae: body.username,
},
};
})
.catch((e) => {
console.error(e);
set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR;
Expand Down
63 changes: 63 additions & 0 deletions src/routes/user/info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import e from "@edgedb";
import Elysia, { t } from "elysia";
import { HttpStatusCode } from "elysia-http-status-code";
import { client } from "index";
import { auth } from "plugins/auth";
import { promiseResult } from "utils/errors";
import { replaceDateWithTimestamp } from "utils/objects/transform";
import { responseBuilder } from "utils/response";
import { userOwnInfoRouter } from "./me";

export const userInfoRouter = new Elysia({ prefix: "/info" })
.use(auth)
.use(HttpStatusCode())
.use(userOwnInfoRouter)
.get(
"/",
async ({ auth, query, set, httpStatus }) => {
const { username } = query;
// The creation date is only returned when the user is requesting information about themselves
const isThemself = auth.isAuthorized && username === auth.username;

const dbQuery = e.select(e.User, (u) => ({
username: true,
displayname: true,
created: isThemself,
filter_single: e.op(u.username, "=", username),
}));

const result = await promiseResult(async () => dbQuery.run(client));

if (result.status !== "success") {
set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR;
return responseBuilder("error", {
error: "Internal server error",
});
}

if (!result.data) {
set.status = httpStatus.HTTP_404_NOT_FOUND;
return responseBuilder("error", {
error: "User not found",
});
}

return responseBuilder("success", {
data: replaceDateWithTimestamp(result.data),
message: "User information retrieved successfully",
});
},
{
query: t.Object({
username: t.String({
minLength: 1,
description: "The username of the user to get information about",
}),
}),
detail: {
description:
"Get information about a user. When authirzing and requesting oneself the creation date will also be received",
tags: ["User"],
},
},
);
22 changes: 22 additions & 0 deletions src/routes/user/me.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { UNAUTHORIZED } from "constants/responses";
import Elysia from "elysia";
import { HttpStatusCode } from "elysia-http-status-code";
import { auth } from "plugins/auth";

export const userOwnInfoRouter = new Elysia({ prefix: "/me" })
.use(auth)
.use(HttpStatusCode())
.get("/", ({ auth, set, httpStatus }) => {
if (!auth.isAuthorized) {
set.status = httpStatus.HTTP_401_UNAUTHORIZED;
return UNAUTHORIZED;
}

// An authorized request to that endpoint will give a lot of info
set.redirect = `/user/info?username=${auth.username}`;
}, {
detail: {
description: "Get information about yourself. For this the user needs to authenthicated",
tags: ["User"],
},
});
24 changes: 24 additions & 0 deletions src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// TODO: Write a unit test

/**
* Executes a provided callback which returns a Promise and handles the result.
* @template T - The type of data returned by the Promise.
* @param callback - The callback function that returns a Promise.
* @returns A Promise that resolves to an object with status "success" and the data returned by the callback, or an object with status "error" if the Promise is rejected.
*/
export const promiseResult = async <T>(callback: () => Promise<T>) => {
return callback()
.then(
(data) =>
({
status: "success",
data,
}) as const,
)
.catch(
() =>
({
status: "error",
}) as const,
);
};
27 changes: 27 additions & 0 deletions src/utils/objects/transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
type Object = Record<string | number | symbol, unknown>;

type ConvertDatesOther<T extends Object, R> = {
[K in keyof T]: T[K] extends Date ? R : T[K];
};

type DateCallback<T> = (val: Date) => T;

export const replaceDate = <T extends Object, R>(
obj: T,
callback: DateCallback<R>,
): ConvertDatesOther<T, R> => {
const pairs = Object.entries(obj);
const newPairs: [string, unknown][] = [];
for (const [key, val] of pairs) {
if (val instanceof Date) newPairs.push([key, callback(val)]);
else newPairs.push([key, val]);
}

return Object.fromEntries(newPairs) as ConvertDatesOther<T, R>;
};

/**
* Replaces all Date objects in an object with their timestamp
*/
export const replaceDateWithTimestamp = <T extends Object>(obj: T) =>
replaceDate(obj, (val) => val.getTime());
19 changes: 19 additions & 0 deletions tests/objects/transform.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, it } from "bun:test";
import { replaceDateWithTimestamp } from "utils/objects/transform";

describe("transform", () => {
it("replaces all dates with timestamps", () => {
const obj = {
date: new Date("2021-01-01"),
number: 1,
string: "hello",
};

const result = replaceDateWithTimestamp(obj);
expect(result).toEqual({
date: 1609459200000,
number: 1,
string: "hello",
});
});
});

0 comments on commit 4ff36d4

Please sign in to comment.