Add services and auth

This commit is contained in:
Aslan 2025-12-23 17:45:51 -05:00
parent 9b0b5dc040
commit 5dec454afb
46 changed files with 900 additions and 31 deletions

View file

@ -0,0 +1,56 @@
import { type FastifyReply, type FastifyRequest } from "fastify";
import type {
ILoginRequest,
IRegisterResponseError,
IRegisterResponseSuccess,
IRegisterRequest,
ILoginResponseError,
ILoginResponseSuccess,
} from "./types.js";
import { loginUser, registerUser } from "../../services/auth/auth.js";
const postRegister = async (request: FastifyRequest, _reply: FastifyReply) => {
const { username, password, email } = request.body as IRegisterRequest;
const newUser = await registerUser({
username: username,
password: password,
email: email,
});
if (!newUser) {
return {
error: "user already exists",
} as IRegisterResponseError;
}
return {
id: newUser.id,
username: newUser.username,
registerDate: newUser.registerDate?.getTime(),
} as IRegisterResponseSuccess;
};
const postLogin = async (request: FastifyRequest, _reply: FastifyReply) => {
const { username, password } = request.body as ILoginRequest;
const session = await loginUser({
username: username,
password: password,
});
if (!session) {
return {
ownerId: "",
error: "incorrect credentials",
} as ILoginResponseError;
}
return {
id: session.id,
ownerId: session.userId,
token: session.token,
} as ILoginResponseSuccess;
};
export { postRegister, postLogin };

View file

@ -0,0 +1,3 @@
export * from "./auth.js";
export * from "./routes.js";
export * from "./types.js";

View file

@ -0,0 +1,9 @@
import { type FastifyInstance } from "fastify";
import * as controller from "./auth.js";
const authRoutes = async (fastify: FastifyInstance) => {
fastify.post(`/register`, controller.postRegister);
fastify.post(`/login`, controller.postLogin);
};
export { authRoutes };

View file

@ -0,0 +1,40 @@
interface IRegisterRequest {
username: string;
password: string;
email?: string;
}
interface IRegisterResponseSuccess {
id: string;
username: string;
registerDate: number;
}
interface IRegisterResponseError {
error: string;
}
interface ILoginRequest {
username: string;
password: string;
}
interface ILoginResponseSuccess {
id: string;
ownerId: string;
token: string;
}
interface ILoginResponseError {
ownerId: string;
error: string;
}
export {
type IRegisterRequest,
type IRegisterResponseSuccess,
type IRegisterResponseError,
type ILoginRequest,
type ILoginResponseSuccess,
type ILoginResponseError,
};

View file

@ -0,0 +1,27 @@
import { type FastifyReply, type FastifyRequest } from "fastify";
import type {
IChannelParams,
IChannelResponseError,
IChannelResponseSuccess,
} from "./types.js";
import { getChannelById } from "../../services/channel/channel.js";
const getChannel = async (request: FastifyRequest, _reply: FastifyReply) => {
const { id } = request.params as IChannelParams;
const channel = await getChannelById(id);
if (!channel) {
return {
id: id,
error: "channel does not exist",
} as IChannelResponseError;
}
return {
id: channel.id,
name: channel.name,
communityId: channel.communityId,
} as IChannelResponseSuccess;
};
export { getChannel };

View file

@ -0,0 +1,3 @@
export * from "./channel.js";
export * from "./routes.js";
export * from "./types.js";

View file

@ -0,0 +1,8 @@
import { type FastifyInstance } from "fastify";
import * as controller from "./channel.js";
const channelRoutes = async (fastify: FastifyInstance) => {
fastify.get(`/:id`, controller.getChannel);
};
export { channelRoutes };

View file

@ -0,0 +1,20 @@
interface IChannelParams {
id: string;
}
interface IChannelResponseError {
id: string;
error: string;
}
interface IChannelResponseSuccess {
id: string;
name: string;
communityId: string;
}
export {
type IChannelParams,
type IChannelResponseError,
type IChannelResponseSuccess,
};

View file

@ -0,0 +1,27 @@
import { type FastifyReply, type FastifyRequest } from "fastify";
import type {
ICommunityParams,
ICommunityResponseError,
ICommunityResponseSuccess,
} from "./types.js";
import { getCommunityById } from "../../services/community/community.js";
const getCommunity = async (request: FastifyRequest, _reply: FastifyReply) => {
const { id } = request.params as ICommunityParams;
const community = await getCommunityById(id);
if (!community) {
return {
id: id,
error: "community does not exist",
} as ICommunityResponseError;
}
return {
id: community.id,
name: community.name,
description: community.description,
} as ICommunityResponseSuccess;
};
export { getCommunity };

View file

@ -0,0 +1,3 @@
export * from "./community.js";
export * from "./routes.js";
export * from "./types.js";

View file

@ -0,0 +1,8 @@
import { type FastifyInstance } from "fastify";
import * as controller from "./community.js";
const communityRoutes = async (fastify: FastifyInstance) => {
fastify.get(`/:id`, controller.getCommunity);
};
export { communityRoutes };

View file

@ -0,0 +1,20 @@
interface ICommunityParams {
id: string;
}
interface ICommunityResponseError {
id: string;
error: string;
}
interface ICommunityResponseSuccess {
id: string;
name: string;
description: string;
}
export {
type ICommunityParams,
type ICommunityResponseError,
type ICommunityResponseSuccess,
};

View file

@ -0,0 +1,3 @@
export * from "./role.js";
export * from "./routes.js";
export * from "./types.js";

View file

@ -0,0 +1,27 @@
import { type FastifyReply, type FastifyRequest } from "fastify";
import type {
IRoleParams,
IRoleResponseError,
IRoleResponseSuccess,
} from "./types.js";
import { getRoleById } from "../../services/role/role.js";
const getRole = async (request: FastifyRequest, _reply: FastifyReply) => {
const { id } = request.params as IRoleParams;
const role = await getRoleById(id);
if (!role) {
return {
id: id,
error: "role does not exist",
} as IRoleResponseError;
}
return {
id: role.id,
name: role.name,
communityId: role.communityId,
} as IRoleResponseSuccess;
};
export { getRole };

View file

@ -0,0 +1,8 @@
import { type FastifyInstance } from "fastify";
import * as controller from "./role.js";
const roleRoutes = async (fastify: FastifyInstance) => {
fastify.get(`/:id`, controller.getRole);
};
export { roleRoutes };

View file

@ -0,0 +1,16 @@
interface IRoleParams {
id: string;
}
interface IRoleResponseError {
id: string;
error: string;
}
interface IRoleResponseSuccess {
id: string;
name: string;
communityId: string;
}
export { type IRoleParams, type IRoleResponseError, type IRoleResponseSuccess };

View file

@ -0,0 +1,3 @@
export * from "./session.js";
export * from "./routes.js";
export * from "./types.js";

View file

@ -0,0 +1,8 @@
import { type FastifyInstance } from "fastify";
import * as controller from "./session.js";
const sessionRoutes = async (fastify: FastifyInstance) => {
fastify.get(`/:id`, controller.getSession);
};
export { sessionRoutes };

View file

@ -0,0 +1,26 @@
import { type FastifyReply, type FastifyRequest } from "fastify";
import type {
ISessionParams,
ISessionResponseError,
ISessionResponseSuccess,
} from "./types.js";
import { getSessionById } from "../../services/session/session.js";
const getSession = async (request: FastifyRequest, _reply: FastifyReply) => {
const { id } = request.params as ISessionParams;
const session = await getSessionById(id);
if (!session) {
return {
id: id,
error: "session does not exist",
} as ISessionResponseError;
}
return {
id: session.id,
userId: session.userId,
} as ISessionResponseSuccess;
};
export { getSession };

View file

@ -0,0 +1,19 @@
interface ISessionParams {
id: string;
}
interface ISessionResponseError {
id: string;
error: string;
}
interface ISessionResponseSuccess {
id: string;
userId: string;
}
export {
type ISessionParams,
type ISessionResponseError,
type ISessionResponseSuccess,
};

View file

@ -2,7 +2,8 @@ import { type FastifyInstance } from "fastify";
import * as controller from "./test.js";
const testRoutes = async (fastify: FastifyInstance) => {
fastify.get("/test", controller.test);
fastify.get("/ping", controller.getPing);
fastify.get("/test", controller.getTest);
};
export { testRoutes };

View file

@ -1,10 +1,11 @@
import { type FastifyReply, type FastifyRequest } from "fastify";
import { testdb } from "../../store/store.js";
const test = async (request: FastifyRequest, reply: FastifyReply) => {
testdb();
return [{ name: "Alice" }];
const getPing = async (_request: FastifyRequest, _reply: FastifyReply) => {
return [{ message: "pong" }];
};
export { test };
const getTest = async (_request: FastifyRequest, _reply: FastifyReply) => {
return [{ message: "ok" }];
};
export { getPing, getTest };

View file

@ -0,0 +1,3 @@
export * from "./user.js";
export * from "./routes.js";
export * from "./types.js";

View file

@ -0,0 +1,9 @@
import { type FastifyInstance } from "fastify";
import * as controller from "./user.js";
const userRoutes = async (fastify: FastifyInstance) => {
fastify.get(`/:id`, controller.getUser);
fastify.get(`/:id/sessions`, controller.getSessions);
};
export { userRoutes };

View file

@ -0,0 +1,41 @@
interface IUserParams {
id: string;
}
interface IUserResponseError {
id: string;
error: string;
}
interface IUserResponseSuccess {
id: string;
username: string;
email: string;
description: string;
admin: boolean;
registerDate: number;
lastLogin: number;
}
interface ISessionsResponseError {
id: string;
error: string;
}
interface ISessionsResponseSuccess {
sessions: ISessionsResponseSession[];
}
interface ISessionsResponseSession {
id: string;
userId: string;
}
export {
type IUserParams,
type IUserResponseError,
type IUserResponseSuccess,
type ISessionsResponseError,
type ISessionsResponseSuccess,
type ISessionsResponseSession,
};

View file

@ -0,0 +1,53 @@
import { type FastifyReply, type FastifyRequest } from "fastify";
import type {
IUserParams,
IUserResponseError,
IUserResponseSuccess,
ISessionsResponseError,
ISessionsResponseSuccess,
} from "./types.js";
import { getUserById, getUserSessionsById } from "../../services/user/user.js";
const getUser = async (request: FastifyRequest, _reply: FastifyReply) => {
const { id } = request.params as IUserParams;
const user = await getUserById(id);
if (!user) {
return {
id: id,
error: "user does not exist",
} as IUserResponseError;
}
return {
id: user.id,
username: user.username,
email: user.email,
description: user.description,
admin: user.admin,
registerDate: user.registerDate.getTime(),
lastLogin: user.lastLogin?.getTime() ?? 0,
} as IUserResponseSuccess;
};
const getSessions = async (request: FastifyRequest, _reply: FastifyReply) => {
const { id } = request.params as IUserParams;
const authHeader = request.headers["authorization"];
const sessions = await getUserSessionsById(id, authHeader);
if (!sessions) {
return {
id: id,
error: "user does not exist or you have no access",
} as ISessionsResponseError;
}
return {
sessions: sessions.map((session) => ({
id: session.id,
userId: session.userId,
})),
} as ISessionsResponseSuccess;
};
export { getUser, getSessions };

59
src/helpers.ts Normal file
View file

@ -0,0 +1,59 @@
import jwt from "jsonwebtoken";
import type { Session, User } from "./generated/prisma/client.js";
import { getDB } from "./store/store.js";
const getJwtSecret = () => {
return process.env.JWT_SECRET || "";
};
const verifyToken = (token: string): string | jwt.JwtPayload | null => {
try {
return jwt.verify(token, getJwtSecret());
} catch {
return null;
}
};
const getSessionFromToken = async (token: string): Promise<Session | null> => {
return await getDB().session.findFirst({
where: {
token: token,
},
});
};
const getUserFromToken = async (token: string): Promise<User | null> => {
const session = await getSessionFromToken(token);
return await getDB().user.findFirst({
where: {
id: session?.userId ?? "invalid",
},
});
};
const getUserFromAuth = async (
authHeader: string | undefined,
): Promise<User | null> => {
const token = authHeader?.replace("Bearer ", "");
const verified = verifyToken(token ?? "") !== null;
if (!verified || !token) {
return null;
}
const user = await getUserFromToken(token);
if (!user) {
return null;
}
return user;
};
export {
getJwtSecret,
verifyToken,
getSessionFromToken,
getUserFromToken,
getUserFromAuth,
};

View file

@ -2,12 +2,24 @@ import { config } from "./config.js";
import Fastify from "fastify";
import { testRoutes } from "./controllers/test/routes.js";
import { authRoutes } from "./controllers/auth/routes.js";
import { userRoutes } from "./controllers/user/routes.js";
import { sessionRoutes } from "./controllers/session/routes.js";
import { communityRoutes } from "./controllers/community/routes.js";
import { channelRoutes } from "./controllers/channel/routes.js";
import { roleRoutes } from "./controllers/role/routes.js";
const app = Fastify({
logger: true,
});
app.register(testRoutes);
app.register(authRoutes, { prefix: "/api/v1/auth" });
app.register(userRoutes, { prefix: "/api/v1/user" });
app.register(sessionRoutes, { prefix: "/api/v1/session" });
app.register(communityRoutes, { prefix: "/api/v1/community" });
app.register(channelRoutes, { prefix: "/api/v1/channel" });
app.register(roleRoutes, { prefix: "/api/v1/role" });
app.listen({ port: config.port }, (err, address) => {
if (err) throw err;

72
src/services/auth/auth.ts Normal file
View file

@ -0,0 +1,72 @@
import argon2 from "argon2";
import jwt from "jsonwebtoken";
import type { User, Session } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
import type { IUserLogin, IUserRegistration } from "./types.js";
import { getJwtSecret } from "../../helpers.js";
const registerUser = async (
registration: IUserRegistration,
): Promise<User | null> => {
const existingUser = await getDB().user.findUnique({
where: { username: registration.username },
});
if (existingUser) {
return null;
}
const passwordHash = await hashPassword(registration.password);
let newUser: User | null = null;
try {
newUser = await getDB().user.create({
data: {
username: registration.username,
passwordHash: passwordHash,
email: registration.email ?? null,
},
});
} catch {
return null;
}
return newUser;
};
const loginUser = async (login: IUserLogin): Promise<Session | null> => {
const user = await getDB().user.findUnique({
where: { username: login.username },
});
const passwordCorrect = await argon2.verify(
user?.passwordHash ?? "",
login.password,
);
if (!user || !passwordCorrect) {
return null;
}
return await getDB().session.create({
data: {
token: createToken(user.id),
userId: user.id,
},
});
};
const hashPassword = async (password: string): Promise<string> => {
return await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 2 ** 16,
timeCost: 4,
parallelism: 1,
});
};
const createToken = (userId: string) => {
return jwt.sign({ sub: userId }, getJwtSecret());
};
export { registerUser, loginUser, hashPassword };

View file

@ -0,0 +1,2 @@
export * from "./auth.js";
export * from "./types.js";

View file

@ -0,0 +1,12 @@
interface IUserRegistration {
username: string;
password: string;
email?: string | undefined;
}
interface IUserLogin {
username: string;
password: string;
}
export { type IUserRegistration, type IUserLogin };

View file

@ -0,0 +1,10 @@
import type { Channel } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
const getChannelById = async (id: string): Promise<Channel | null> => {
return await getDB().channel.findUnique({
where: { id: id },
});
};
export { getChannelById };

View file

@ -0,0 +1 @@
export * from "./channel.js";

View file

@ -0,0 +1,10 @@
import type { Community } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
const getCommunityById = async (id: string): Promise<Community | null> => {
return await getDB().community.findUnique({
where: { id: id },
});
};
export { getCommunityById };

View file

@ -0,0 +1 @@
export * from "./community.js";

View file

@ -0,0 +1 @@
export * from "./role.js";

10
src/services/role/role.ts Normal file
View file

@ -0,0 +1,10 @@
import type { Role } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
const getRoleById = async (id: string): Promise<Role | null> => {
return await getDB().role.findUnique({
where: { id: id },
});
};
export { getRoleById };

View file

@ -0,0 +1 @@
export * from "./session.js";

View file

@ -0,0 +1,10 @@
import type { Session } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
const getSessionById = async (id: string): Promise<Session | null> => {
return await getDB().session.findUnique({
where: { id: id },
});
};
export { getSessionById };

View file

@ -0,0 +1 @@
export * from "./user.js";

27
src/services/user/user.ts Normal file
View file

@ -0,0 +1,27 @@
import type { User, Session } from "../../generated/prisma/client.js";
import { getUserFromAuth } from "../../helpers.js";
import { getDB } from "../../store/store.js";
const getUserById = async (id: string): Promise<User | null> => {
return await getDB().user.findUnique({
where: { id: id },
});
};
const getUserSessionsById = async (
id: string,
authHeader: string | undefined,
): Promise<Session[] | null> => {
const user = await getUserFromAuth(authHeader);
if (!user || user.id !== id) {
return null;
}
return await getDB().session.findMany({
where: {
userId: id,
},
});
};
export { getUserById, getUserSessionsById };

View file

@ -1,2 +1 @@
export * from "./store.js";
export * from "./types.js";

View file

@ -10,18 +10,8 @@ const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
async function testdb() {
const test = await prisma.user.findMany();
/*
const user = await prisma.user.create({
data: { name: "Alice", email: `alice${Math.random()}@example.com` },
});
const getDB = (): PrismaClient => {
return prisma;
};
console.log("Created user:", user);
const test = await prisma.user.findMany();
console.log(test);
*/
}
export { testdb };
export { getDB };

View file

@ -1,3 +0,0 @@
interface IState {}
export { type IState };