installed minIO and added Endpoints for creating posts and retrieving a post with the ID

This commit is contained in:
Kai Ritthaler 2025-05-23 21:52:20 +02:00 committed by Rudi Regentonne
parent 38e796ca22
commit ece95c7130
12 changed files with 962 additions and 71 deletions

View file

@ -0,0 +1,143 @@
import express, { Request, Response } from "express";
import dotenv from "dotenv";
import { StatusCodes } from "http-status-codes";
import { JwtPayload } from "jsonwebtoken";
import { PrismaClient, Post } from "@prisma/client";
import { minioClient } from "../server";
import { object } from "zod";
import { uploadPostSchema } from "../schemas/postSchemas";
dotenv.config();
const prisma = new PrismaClient();
export const uploadPost = async (req: Request, res: Response) => {
const files = req.files as Express.Multer.File[]; // Mehrere Dateien // Cast req.file to Express.Multer.File there is no need to check if file is undefined since it is already checked in the middleware
const user: JwtPayload = req.user!; // Get the user from the request
const { description, status, tags } = uploadPostSchema.parse(req.body);
const BUCKET = "images"; // Name of the bucket where the images are stored
console.log(tags);
try {
const uploads = await Promise.all(
files.map(async (file) => {
const objectName = `${user.sub}/${Date.now()}-${file.originalname}`;
await minioClient.putObject(BUCKET, objectName, file.buffer);
const url = await minioClient.presignedGetObject(
BUCKET,
objectName,
60 * 5 // 5 Minuten Gültigkeit
);
return {
originalName: file.originalname,
objectName: objectName,
size: file.size,
mimetype: file.mimetype,
url: url,
};
})
);
const post: Post | null = await prisma.post.create({
data: {
userId: user.sub!,
description: description,
status: status,
media: {
create: uploads.map((upload) => ({
originalFilename: upload.originalName,
objectName: upload.objectName,
size: upload.size,
mimeType: upload.mimetype,
bucket: BUCKET,
uploader: { connect: { id: user.sub! } },
})),
},
postTags: {
create: tags.map((tag: string) => ({
tag: {
connectOrCreate: { where: { name: tag }, create: { name: tag } },
},
})),
},
},
}); // create a new post in the database
res.status(StatusCodes.CREATED).json({
message: "Upload successful",
post: post,
});
} catch (err) {
console.error(err);
res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.json({ error: "Upload failed" });
}
};
// get post from postId
export const getPost = async (req: Request, res: Response) => {
try {
// get the postId from the request
const postId: string = req.query.postId as string;
const postObject = await prisma.post.findUnique({
// find the post by id
where: {
id: postId,
},
include: {
user: true,
media: true,
},
});
if (!postObject) {
res.status(StatusCodes.NOT_FOUND).json({
error: "Post not found",
details: [
{
message: `The Post does not exist`,
},
],
});
return;
}
const post = await Promise.all(
// generate the presigned url for each image
postObject?.media.map(async (image) => {
try {
await minioClient.statObject(image.bucket, image.objectName);
return {
originalName: image.originalFilename,
mimetype: image.mimeType,
url: await minioClient.presignedGetObject(
image.bucket,
image.objectName,
60 * 5
),
};
} catch (err) {
return null;
}
}) ?? []
);
res.status(StatusCodes.OK).json({
message: "Post found",
post: post,
});
} catch (err: any) {
if (err.code === "NotFound") {
// Handle the case where the object does not exist
res.status(StatusCodes.NOT_FOUND).json({
error: "Image not found",
details: [
{
message: `The image does not exist in the bucket `,
},
],
});
}
console.error(err);
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: "Failed to retrieve post",
details: [{ message: "Server error" }],
});
}
};

View file

@ -1,6 +1,6 @@
import express, { Request, Response } from "express";
import { PrismaClient } from "@prisma/client";
import { UserLoginDto, userLoginSchema } from "../schemas/userSchemas";
import { PrismaClient, User } from "@prisma/client";
import { UserLoginDto, UserRegistrationDto } from "../schemas/userSchemas";
import jwt from "jsonwebtoken";
import dotenv from "dotenv";
import bcrypt from "bcryptjs";
@ -14,19 +14,18 @@ dotenv.config();
const JWT_SECRET: string = process.env.TOKEN_SECRET!; // this secret is used to sign the JWT token
// Generate a JWT token with the username as payload and a secret from the environment variables which expires in 1800 seconds (30 minutes)
function generateAccessToken(username: string, userId: string) {
return jwt.sign(
{ username: username, role: "user", sub: userId },
JWT_SECRET,
{ expiresIn: "1800s", issuer: "VogelApi" }
); //TODO: change role to user role
function generateAccessToken(username: string, userId: string, role: string) {
return jwt.sign({ username: username, role: role, sub: userId }, JWT_SECRET, {
expiresIn: "1800s",
issuer: "VogelApi",
});
}
// Endpoint to register a new user
export const registerUser = async (req: Request, res: Response) => {
const { username, password, email } = await req.body; //gets the data from the request body
const { username, password, email }: UserRegistrationDto = await req.body; //gets the data from the request body
const existingUser = await prisma.user.findUnique({
const existingUser: User | null = await prisma.user.findUnique({
// check if the user already exists
where: {
username: username,
@ -40,6 +39,21 @@ export const registerUser = async (req: Request, res: Response) => {
});
return;
}
const existingEmailUser: User | null = await prisma.user.findUnique({
// check if the email is already in use
where: {
email: email,
},
});
if (existingEmailUser) {
// if the email is already in use, return an error message
res.status(StatusCodes.BAD_REQUEST).json({
error: "Invalid data",
details: [{ message: `User with "${email}" already exists` }],
});
return;
}
const hashedPassword = await bcrypt.hash(password, 10); // hash the password with bcrypt
if (!hashedPassword) {
// check if the password was hashed successfully
@ -55,7 +69,7 @@ export const registerUser = async (req: Request, res: Response) => {
email: email,
password: hashedPassword,
};
const user = await prisma.user.create({ data: userData }); // create a new user in the database
const user: User | null = await prisma.user.create({ data: userData }); // create a new user in the database
if (!user) {
// check if the user was created successfully
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
@ -64,7 +78,7 @@ export const registerUser = async (req: Request, res: Response) => {
});
return;
}
const token: string = generateAccessToken(user.username, user.id); // generate a JWT token with the username and userId as payload
const token: string = generateAccessToken(user.username, user.id, user.role); // generate a JWT token with the username and userId as payload
res.set("Authorization", `Bearer ${token}`); // set the token in the response header
res.status(StatusCodes.CREATED).json({
message: "user created",
@ -74,9 +88,9 @@ export const registerUser = async (req: Request, res: Response) => {
// Endpoint to login a user (unfinished)
export const loginUser = async (req: Request, res: Response) => {
const { username, password } = req.body; // get the data from the request body
const { username, password }: UserLoginDto = req.body; // get the data from the request body
const user = await prisma.user.findUnique({
const user: User | null = await prisma.user.findUnique({
// check if the user exists
where: {
username: username,
@ -99,7 +113,7 @@ export const loginUser = async (req: Request, res: Response) => {
});
return;
}
const token: string = generateAccessToken(user.username, user.id); // generate a JWT token with the username and userId as payload
const token: string = generateAccessToken(user.username, user.id, user.role); // generate a JWT token with the username and userId as payload
res.set("Authorization", `Bearer ${token}`); // set the token in the response header
res.status(StatusCodes.OK).json({ message: "User logged in successfully" });
};
@ -114,7 +128,7 @@ export const getUser = async (req: Request, res: Response) => {
});
return;
}
const user = await prisma.user.findUnique({
const user: User | null = await prisma.user.findUnique({
where: {
username: username,
},
@ -128,6 +142,11 @@ export const getUser = async (req: Request, res: Response) => {
}
res.json({
message: "User found",
data: { username: user.username, email: user.email },
data: {
username: user.username,
email: user.email,
userId: user.id,
userInfo: user.bio,
},
});
};

View file

@ -3,18 +3,21 @@ import jwt, { TokenExpiredError } from "jsonwebtoken";
import dotenv from "dotenv";
import { StatusCodes } from "http-status-codes";
dotenv.config();
// imports the JWT secret
// Import the JWT secret from environment variables
const JWT_SECRET: string = process.env.TOKEN_SECRET!;
if (!JWT_SECRET) console.log("no JWT secret");
// create an interface for the JWT payload
// this interface is used to define the structure of the JWT payload
if (!JWT_SECRET) console.error("No JWT secret provided");
// Define the structure of the JWT payload
interface JwtPayload {
id: string;
username: string;
role: string;
iat: number;
exp: number;
}
// extend the Express Request interface to include the user property
// this is used to store the JWT payload in the request object
// Extend the Express Request interface to include the user property
declare global {
namespace Express {
interface Request {
@ -22,39 +25,44 @@ declare global {
}
}
}
// Middleware function to authenticate the JWT token
export function authenticateToken() {
return (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers["authorization"]; // get the authorization header from the request
const token = authHeader && authHeader.split(" ")[1]; // split the header to get the token
const authHeader = req.headers["authorization"]; // Get the authorization header
const token = authHeader && authHeader.split(" ")[1]; // Extract the token from the "Bearer <token>" format
if (token == null)
res.sendStatus(StatusCodes.UNAUTHORIZED); // if there is no token, return 401 Unauthorized
else {
jwt.verify(token, JWT_SECRET, (err: any, user: any) => {
// verify the token with the secret
if (err) {
if (err instanceof TokenExpiredError) {
// check if the error is expired and return 401
res.status(StatusCodes.UNAUTHORIZED).json({
error: "Token expired",
details: [{ message: "Token expired" }],
});
return;
}
// if the token is invalid, return 403 Forbidden
else {
res.status(StatusCodes.FORBIDDEN).json({
error: "Invalid token",
details: [{ message: "Invalid token" }],
});
return;
}
}
next();
if (!token) {
res.status(StatusCodes.UNAUTHORIZED).json({
error: "Unauthorized",
details: [{ message: "No token provided" }],
});
return;
}
jwt.verify(token, JWT_SECRET, (err, decoded) => {
if (err) {
if (err instanceof TokenExpiredError) {
// Handle expired token
res.status(StatusCodes.UNAUTHORIZED).json({
error: "Token expired",
details: [{ message: "Token has expired" }],
});
return;
}
// Handle invalid token
res.status(StatusCodes.FORBIDDEN).json({
error: "Invalid token",
details: [{ message: "Token is invalid" }],
});
return;
}
// Attach the decoded payload to the request object
req.user = decoded as JwtPayload;
next(); // Pass control to the next middleware or route handler
});
};
}

View file

@ -0,0 +1,52 @@
import { StatusCodes } from "http-status-codes";
import multer, { MulterError } from "multer";
import { Request, Response, NextFunction } from "express";
// Configure multer to store files in memory
const multerInstance = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024 }, // Limit file size to 5 MB
});
export const upload = (req: Request, res: Response, next: NextFunction) => {
multerInstance.array("images")(req, res, (err: any) => {
if (err) {
console.error(err);
return res
.status(StatusCodes.BAD_REQUEST)
.json({ error: err.name, details: [err.message] });
}
if (!req.files || !(req.files as Express.Multer.File[]).length) {
//check if user uploaded files
return res.status(StatusCodes.BAD_REQUEST).json({
error: "No files uploaded",
details: [{ message: "Please upload at least one file" }],
});
}
const files = req.files as Express.Multer.File[];
for (const file of files) {
//check for correct filetypes
if (
file.mimetype !== "image/jpeg" &&
file.mimetype !== "image/png" &&
file.mimetype !== "image/webp"
) {
return res.status(StatusCodes.BAD_REQUEST).json({
error: "Invalid file type",
details: [
{
message:
"Only .jpeg, .png, or .webp files are allowed. Invalid: " +
file.originalname,
},
],
});
}
}
next();
});
};

View file

@ -21,6 +21,7 @@ export function validateData(schema: z.ZodObject<any, any>) {
res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.json({ error: "Internal Server Error" });
console.error("Unexpected error:", error);
return;
}
}

View file

@ -0,0 +1,89 @@
import express from "express";
import {
getPost,
uploadPost as uploadPost,
} from "../controllers/postController";
import { upload } from "../middleware/fileUpload";
import { authenticateToken } from "../middleware/authenticateToken";
import { validateData } from "../middleware/validationMiddleware";
import { uploadPostSchema } from "../schemas/postSchemas";
const router = express.Router();
/**
* @swagger
* /api/posts/upload:
* post:
* summary: Upload multiple images with metadata
* tags:
* - posts
* requestBody:
* required: true
* content:
* multipart/form-data:
* schema:
* type: object
* required:
* - images
* - description
* - status
* properties:
* images:
* type: array
* items:
* type: string
* format: binary
* status:
* type: string
* enum: [HIDDEN, PUBLIC, PRIVATE, ARCHIVED]
* description:
* type: string
* tags:
* type: array
* items:
* type: string
* encoding:
* images:
* style: form
* explode: true
* tags:
* style: form
* explode: true
* status:
* style: form
* description:
* style: form
* responses:
* 200:
* description: Images uploaded successfully
*/
router.post(
"/upload",
authenticateToken(),
upload,
validateData(uploadPostSchema),
uploadPost
);
/**
* @swagger
* /api/posts/get/{postId}:
* get:
* summary: Get Post
* tags: [posts]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: postId
* required: true
* schema:
* type: string
* description: The post id
* responses:
* 200:
* description: Ok
* 401:
* description: not authenticated
*/
router.get("/get/:postId", getPost);
export default router;

View file

@ -0,0 +1,26 @@
import { z } from "zod";
export const PostStatusEnum = z.enum([
"HIDDEN",
"PUBLIC",
"PRIVATE",
"ARCHIVED",
]);
export const uploadPostSchema = z.object({
description: z.string(),
status: PostStatusEnum,
// this is hilarious but it works
// if the value is a string, convert it to an array
// if the value is an array, return it as is
// this is just a workaround for the fact that swagger is not fucking able to handle arrays
tags: z.preprocess((val) => {
if (typeof val === "string") {
return [val]; // Single tag string
} else if (Array.isArray(val)) {
return val; // Multiple tags
} else {
return []; // Optional: fallback for undefined/null
}
}, z.array(z.string())),
});

View file

@ -1,7 +1,9 @@
import express, { Request, Response, Application } from "express";
import { Client } from "minio";
import dotenv from "dotenv";
import userRouter from "./routes/userRoutes";
import postRouter from "./routes/postRoutes";
//import postController from "./controllers/postController";
import bodyParser from "body-parser";
dotenv.config();
@ -9,6 +11,14 @@ dotenv.config();
const app = express();
const port = 3000;
// minIO config
export const minioClient = new Client({
endPoint: "localhost", // Replace with your MinIO server URL
port: 9000, // Default MinIO port
useSSL: false, // Set to true if using HTTPS
accessKey: process.env.MINIO_USER, // minIO username/access key
secretKey: process.env.MINIO_PASSWORD, // MinIO password/secret key
});
//swagger configuration
import swaggerJSDoc from "swagger-jsdoc";
import swaggerUi from "swagger-ui-express";
@ -49,10 +59,8 @@ app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs));
app.use(bodyParser.json());
app.use("/api/user", userRouter);
// Sample route
app.get("/api/hello", (req, res) => {
res.send("Hello World!");
});
app.use("/api/posts", postRouter);
app.listen(port, () => {
console.log(`Server läuft auf http://localhost:${port}`);
});