mirror of
https://github.com/bubblecup-12/VogelSocialMedia.git
synced 2025-07-07 06:08:54 +00:00
installed minIO and added Endpoints for creating posts and retrieving a post with the ID
This commit is contained in:
parent
38e796ca22
commit
ece95c7130
12 changed files with 962 additions and 71 deletions
143
code/backend/src/controllers/postController.ts
Normal file
143
code/backend/src/controllers/postController.ts
Normal 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" }],
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
52
code/backend/src/middleware/fileUpload.ts
Normal file
52
code/backend/src/middleware/fileUpload.ts
Normal 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();
|
||||
});
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
89
code/backend/src/routes/postRoutes.ts
Normal file
89
code/backend/src/routes/postRoutes.ts
Normal 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;
|
26
code/backend/src/schemas/postSchemas.ts
Normal file
26
code/backend/src/schemas/postSchemas.ts
Normal 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())),
|
||||
});
|
|
@ -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}`);
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue