added endpoints for profile and feed

This commit is contained in:
Kai Ritthaler 2025-06-26 11:10:52 +02:00 committed by mrProm3theus
parent 9e6eeb27fd
commit 1800056918
22 changed files with 952 additions and 110 deletions

View file

@ -20,7 +20,6 @@
}, },
"devDependencies": { "devDependencies": {
"@prisma/client": "^6.9.0", "@prisma/client": "^6.9.0",
"@types/bcryptjs": "^3.0.0",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.1", "@types/express": "^5.0.1",
"@types/jsonwebtoken": "^9.0.9", "@types/jsonwebtoken": "^9.0.9",

View file

@ -89,7 +89,7 @@ model Comment {
} }
model Like { model Like {
id String @id @default(uuid()) @@id([postId, userId])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
post Post @relation(fields: [postId], references: [id]) post Post @relation(fields: [postId], references: [id])

View file

@ -0,0 +1,31 @@
import express, { Request, Response } from "express";
import { StatusCodes } from "http-status-codes";
import { PrismaClient, Post } from "../../prisma/app/generated/prisma/client";
import { feedQuerySchema } from "../schemas/feedSchemas";
const prisma = new PrismaClient();
export const feed = async (req: Request, res: Response) => {
try {
const query = feedQuerySchema.parse(req.query);
const take = query.limit || 10;
const posts = await prisma.post.findMany({
take: take + 1,
where: {
status: "PUBLIC",
...(query.createdAt && { createdAt: { lt: query.createdAt } }),
},
orderBy: { createdAt: "desc" },
select: { id: true, createdAt: true, description: true },
});
const hasMore = posts.length > take;
if (hasMore) posts.pop();
const nextCursor = hasMore ? posts[posts.length - 1].createdAt : null;
res.status(200).json({ posts, nextCursor });
} catch (err) {
console.error(err);
res.status(400).json({ error: "Invalid query parameters" });
}
};

View file

@ -0,0 +1,124 @@
import { StatusCodes } from "http-status-codes";
import express, { Request, Response } from "express";
import { JwtPayload } from "jsonwebtoken";
import { PrismaClient, Follow } from "../../prisma/app/generated/prisma/client";
const prisma = new PrismaClient();
export const followUser = async (req: Request, res: Response) => {
const username: string = req.params.username;
const followingUser: JwtPayload = req.user!;
if (!username) {
res.status(StatusCodes.BAD_REQUEST).json({
error: "no username",
details: [{ message: "Username is required" }],
});
return;
}
try {
const user = await prisma.user.findUnique({
where: { username },
});
if (!user) {
res.status(StatusCodes.NOT_FOUND).json({
error: "User not found",
details: [
{ message: `User with username '${username}' does not exist.` },
],
});
return;
}
if (user.username == username) {
res.status(StatusCodes.BAD_REQUEST).json({
error: "Bad Request",
details: [{ message: "You can`t follow yourself" }],
});
return;
}
const alreadyFollowing = await prisma.follow.findUnique({
where: {
followingUserId_followedUserId: {
followingUserId: followingUser.sub!,
followedUserId: user.id,
},
},
});
if (alreadyFollowing) {
res.status(StatusCodes.CONFLICT).json({
error: "Already following",
details: [{ message: "You are already following this User" }],
});
return;
}
const follow = await prisma.follow.create({
data: {
followingUserId: followingUser.sub!,
followedUserId: user.id,
},
});
res.status(StatusCodes.NO_CONTENT).send();
return;
} catch (err) {
console.error(err);
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: "Upload failed",
details: [{ message: "Internal server error" }],
});
}
};
export const unfollowUser = async (req: Request, res: Response) => {
const username: string = req.params.username;
const followingUser: JwtPayload = req.user!;
if (!username) {
res.status(StatusCodes.BAD_REQUEST).json({
error: "no username",
details: [{ message: "Username is required" }],
});
return;
}
try {
const user = await prisma.user.findUnique({
where: { username },
});
if (!user) {
res.status(StatusCodes.NOT_FOUND).json({
error: "User not found",
details: [
{ message: `User with username '${username}' does not exist.` },
],
});
return;
}
const alreadyFollowing = await prisma.follow.findUnique({
where: {
followingUserId_followedUserId: {
followingUserId: followingUser.sub!,
followedUserId: user.id,
},
},
});
if (!alreadyFollowing) {
res.status(StatusCodes.CONFLICT).json({
error: "Not following",
details: [{ message: "You are not following this User" }],
});
return;
}
const follow = await prisma.follow.delete({
where: {
followingUserId_followedUserId: {
followingUserId: followingUser.sub!,
followedUserId: user.id,
},
},
});
res.status(StatusCodes.NO_CONTENT).send();
return;
} catch (err) {
console.error(err);
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: "Upload failed",
details: [{ message: "Internal server error" }],
});
}
};

View file

@ -17,12 +17,12 @@ export const uploadPost = async (req: Request, res: Response) => {
try { try {
const uploads = await Promise.all( const uploads = await Promise.all(
files.map(async (file) => { files.map(async (file) => {
const objectName = `${user.sub}/${Date.now()}-${file.originalname}`; const objectName = `${user.sub}/posts/${Date.now()}-${file.originalname}`;
await minioClient.putObject(BUCKET, objectName, file.buffer); await minioClient.putObject(BUCKET, objectName, file.buffer);
const url = await minioClient.presignedGetObject( const url = await minioClient.presignedGetObject(
BUCKET, BUCKET,
objectName, objectName,
60 * 5 // 5 Minuten Gültigkeit 60 * 10
); );
return { return {
@ -76,10 +76,19 @@ export const getPost = async (req: Request, res: Response) => {
try { try {
// get the postId from the request // get the postId from the request
const postId: string = req.query.postId as string; const postId: string = req.query.postId as string;
const postObject = await prisma.post.findUnique({ const user: JwtPayload | undefined = req.user;
const postObject = await prisma.post.findFirst({
// find the post by id // find the post by id
where: { where: {
id: postId, id: postId,
...(user
? {
OR: [
{ status: "PRIVATE", userId: user.id },
{ status: "PUBLIC" },
],
}
: { status: "PUBLIC" }),
}, },
include: { include: {
user: true, user: true,
@ -107,6 +116,20 @@ export const getPost = async (req: Request, res: Response) => {
}); });
return; return;
} }
let isFollowing = false;
if (user) {
const following = await prisma.follow.findUnique({
where: {
followingUserId_followedUserId: {
followingUserId: user.sub!,
followedUserId: postObject.userId,
},
},
});
if (following) {
isFollowing = true;
}
}
const images = await Promise.all( const images = await Promise.all(
// generate the presigned url for each image // generate the presigned url for each image
postObject?.media.map(async (image) => { postObject?.media.map(async (image) => {
@ -139,7 +162,9 @@ export const getPost = async (req: Request, res: Response) => {
createdAt: postObject.createdAt, createdAt: postObject.createdAt,
updatedAt: postObject.updatedAt, updatedAt: postObject.updatedAt,
images: images.filter((image) => image !== null), // filter out the null images images: images.filter((image) => image !== null), // filter out the null images
following: isFollowing,
}); });
return;
} catch (err: any) { } catch (err: any) {
if (err.code === "NotFound") { if (err.code === "NotFound") {
// Handle the case where the object does not exist // Handle the case where the object does not exist
@ -151,22 +176,26 @@ export const getPost = async (req: Request, res: Response) => {
}, },
], ],
}); });
return;
} }
console.error(err); console.error(err);
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: "Failed to retrieve post", error: "Failed to retrieve post",
details: [{ message: "Server error" }], details: [{ message: "Server error" }],
}); });
return;
} }
}; };
// get all posts from a user // get all posts from a user
export const getUserPosts = async (req: Request, res: Response) => { export const getUserPosts = async (req: Request, res: Response) => {
try { try {
const userId: string = req.query.userId as string; // Get the userId from the request const username: string = req.params.username;
const posts = await prisma.post.findMany({ const posts = await prisma.post.findMany({
where: { where: {
userId: userId, user: {
username: username, // hier greifst du auf die relationierte User-Tabelle zu
},
}, },
}); });
if (!posts || posts.length === 0) { if (!posts || posts.length === 0) {
@ -192,3 +221,111 @@ export const getUserPosts = async (req: Request, res: Response) => {
return; return;
} }
}; };
export const like = async (req: Request, res: Response) => {
const postId: string = req.params.postId;
const user: JwtPayload = req.user!;
if (!postId) {
res.status(StatusCodes.BAD_REQUEST).json({
error: "no username",
details: [{ message: "Username is required" }],
});
return;
}
try {
const post = await prisma.post.findUnique({
where: { id: postId },
});
if (!post) {
res.status(StatusCodes.NOT_FOUND).json({
error: "Post not found",
details: [{ message: `Post with PostId '${postId}' does not exist.` }],
});
return;
}
const alreadyLiked = await prisma.like.findUnique({
where: {
postId_userId: {
postId: postId,
userId: user.sub!,
},
},
});
if (alreadyLiked) {
res.status(StatusCodes.CONFLICT).json({
error: "Already following",
details: [{ message: "You are already following this User" }],
});
return;
}
const follow = await prisma.like.create({
data: {
postId: postId,
userId: user.sub!,
},
});
res.status(StatusCodes.NO_CONTENT).send();
return;
} catch (err) {
console.error(err);
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: "Upload failed",
details: [{ message: "Internal server error" }],
});
}
};
export const removeLike = async (req: Request, res: Response) => {
const postId: string = req.params.postId;
const user: JwtPayload = req.user!;
if (!postId) {
res.status(StatusCodes.BAD_REQUEST).json({
error: "no postId",
details: [{ message: "postId is required" }],
});
return;
}
try {
const post = await prisma.post.findUnique({
where: { id: postId },
});
if (!post) {
res.status(StatusCodes.NOT_FOUND).json({
error: "Post not found",
details: [{ message: `Post with PostId '${postId}' does not exist.` }],
});
return;
}
const alreadyLiked = await prisma.like.findUnique({
where: {
postId_userId: {
postId: postId!,
userId: user.sub!,
},
},
});
if (!alreadyLiked) {
res.status(StatusCodes.CONFLICT).json({
error: "Already following",
details: [{ message: "You are already following this User" }],
});
return;
}
const follow = await prisma.like.delete({
where: {
postId_userId: {
postId: postId,
userId: user.sub!,
},
},
});
res.status(StatusCodes.NO_CONTENT).send();
return;
} catch (err) {
console.error(err);
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: "Upload failed",
details: [{ message: "Internal server error" }],
});
}
};

View file

@ -0,0 +1,235 @@
import express, { Request, Response } from "express";
import { JwtPayload } from "jsonwebtoken";
import { PrismaClient } from "../../prisma/app/generated/prisma/client";
import dotenv from "dotenv";
import { StatusCodes } from "http-status-codes";
import multer from "multer";
import { minioClient } from "../server";
const app = express();
app.use(express.json());
const prisma = new PrismaClient();
// load environment variables from .env file
dotenv.config();
type PublicUser = {
id: string;
username: string;
bio: string | null;
profilePictureUrl: string | null;
followers: number;
following: number;
};
const getUser: (userId: string) => Promise<PublicUser | undefined> = async (
userId: string
) => {
const user = await prisma.user.findUnique({
where: { id: userId },
include: { profilePicture: true },
});
if (user) {
let profilePictureUrl: string | null = null;
if (user.profilePicture) {
profilePictureUrl = await minioClient.presignedGetObject(
user.profilePicture.bucket,
user.profilePicture.objectName,
60 * 10
);
}
const followerCount = await prisma.follow.count({
where: {
followedUserId: userId,
},
});
const followingCount = await prisma.follow.count({
where: {
followingUserId: userId,
},
});
const publicUser: PublicUser = {
id: user.id,
username: user.username,
bio: user.bio,
profilePictureUrl,
followers: followerCount,
following: followingCount,
};
return publicUser;
}
return undefined;
};
export const uploadProfilePicture = async (req: Request, res: Response) => {
const user: JwtPayload = req.user!;
const file = req.file as Express.Multer.File;
const BUCKET = "images"; // Name of the bucket where the images are stored
try {
const objectName = `${user.sub}/profile.${file.mimetype.split("/")[1]}`;
await minioClient.putObject(BUCKET, objectName, file.buffer);
const url = await minioClient.presignedGetObject(
BUCKET,
objectName,
60 * 10
);
const oldImage = await prisma.user.findUnique({
where: { id: user.sub },
select: { profilePictureId: true },
});
const media = await prisma.media.create({
data: {
originalFilename: file.originalname,
objectName: objectName,
mimeType: file.mimetype,
size: file.size,
bucket: BUCKET,
uploader: { connect: { id: user.sub } },
},
});
await prisma.user.update({
where: { id: user.sub },
data: {
profilePicture: {
connect: { id: media.id },
},
},
});
if (oldImage?.profilePictureId && oldImage.profilePictureId !== media.id) {
await prisma.media.delete({
where: { id: oldImage.profilePictureId },
});
}
res.status(StatusCodes.CREATED).json({
originalName: file.originalname,
objectName: objectName,
size: file.size,
mimetype: file.mimetype,
url: url,
});
} catch (err) {
console.error(err);
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: "Upload failed",
details: [{ message: "Internal server error" }],
});
}
};
export const getProfilePicture = async (req: Request, res: Response) => {
const username: string = req.params.username;
if (!username) {
res.status(StatusCodes.BAD_REQUEST).json({
error: "no username",
details: [{ message: "Username is required" }],
});
return;
}
try {
const user = await prisma.user.findUnique({
where: { username },
include: { profilePicture: true },
});
if (!user) {
res.status(StatusCodes.NOT_FOUND).json({
error: "User not found",
details: [
{ message: `User with username '${username}' does not exist.` },
],
});
return;
}
if (!user.profilePicture) {
res.status(StatusCodes.NOT_FOUND).json({
error: "No profile picture",
details: [{ message: "This user does not have a profile picture." }],
});
return;
}
const profilePictureUrl = await minioClient.presignedGetObject(
user.profilePicture.bucket,
user.profilePicture.objectName,
60 * 10 // 10 minutes expiration
);
res.status(StatusCodes.OK).json({ url: profilePictureUrl });
} catch (err) {
console.error(err);
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: "Retrieving image failed",
details: ["Internal server error"],
});
return;
}
};
export const updateBio = async (req: Request, res: Response) => {
const user: JwtPayload = req.user!;
const bio: string = req.body.bio;
try {
const updatedUser = await prisma.user.update({
where: { id: user.sub },
data: {
bio: bio,
},
});
const publicUser: PublicUser | undefined = await getUser(user.sub!);
res.status(StatusCodes.OK).json({
message: "User found",
data: publicUser,
});
} catch (err) {
console.error(err);
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: "Updating bio failed",
details: [{ message: "Internal Server error" }],
});
}
};
// Endpoint to get user data
export const getProfile = async (req: Request, res: Response) => {
const username: string = req.params.username as string;
if (!username) {
res.status(StatusCodes.BAD_REQUEST).json({
error: "no username",
details: [{ message: "Username is required" }],
});
return;
}
try {
const user = await prisma.user.findUnique({
where: { username },
include: { profilePicture: true },
});
if (!user) {
res.status(StatusCodes.NOT_FOUND).json({
error: "User not found",
details: [
{ message: `User with username '${username}' does not exist.` },
],
});
return;
}
const publicUser: PublicUser | undefined = await getUser(user.id);
res.status(StatusCodes.OK).json({
message: "User found",
data: publicUser,
});
} catch (err) {
console.error(err);
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: "Retrieving image failed",
details: ["Error while retrieving the image"],
});
return;
}
};

View file

@ -171,37 +171,6 @@ export const loginUser = async (req: Request, res: Response) => {
res.status(StatusCodes.OK).json({ message: "User logged in successfully" }); res.status(StatusCodes.OK).json({ message: "User logged in successfully" });
}; };
// Endpoint to get user data
export const getUser = async (req: Request, res: Response) => {
const username: string = req.params.username as string;
if (!username) {
res.status(StatusCodes.BAD_REQUEST).json({
error: "no username",
details: [{ message: "Username is required" }],
});
return;
}
const user: User | null = await prisma.user.findUnique({
where: {
username: username,
},
});
if (!user) {
res.status(StatusCodes.NOT_FOUND).json({
error: "user not found",
details: [{ message: `User "${username}" not found` }],
});
return;
}
res.json({
message: "User found",
data: {
username: user.username,
userId: user.id,
bio: user.bio,
},
});
};
export const refreshToken = async (req: Request, res: Response) => { export const refreshToken = async (req: Request, res: Response) => {
const refreshToken: string | undefined = req.headers[ const refreshToken: string | undefined = req.headers[
"refresh-token" "refresh-token"

View file

@ -9,17 +9,6 @@ dotenv.config();
const JWT_SECRET: string = process.env.TOKEN_SECRET!; const JWT_SECRET: string = process.env.TOKEN_SECRET!;
if (!JWT_SECRET) console.error("No JWT secret provided"); if (!JWT_SECRET) console.error("No JWT secret provided");
// Extend the Express Request interface to include the user property
declare global {
namespace Express {
interface Request {
user?: JwtPayload;
}
}
}
// Middleware function to authenticate the JWT token // Middleware function to authenticate the JWT token
export function authenticateToken() { export function authenticateToken() {
return (req: Request, res: Response, next: NextFunction) => { return (req: Request, res: Response, next: NextFunction) => {

View file

@ -0,0 +1,25 @@
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import { JwtPayload } from "../types/tokens";
const JWT_SECRET: string = process.env.TOKEN_SECRET!;
export const optionalAuthenticateToken = (
req: Request,
res: Response,
next: NextFunction
) => {
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) {
return next();
}
jwt.verify(token!, JWT_SECRET, (err, user) => {
if (err) {
return next();
}
req.user = user as JwtPayload;
next();
});
};

View file

@ -1,5 +1,5 @@
import { StatusCodes } from "http-status-codes"; import { StatusCodes } from "http-status-codes";
import multer, { MulterError } from "multer"; import multer from "multer";
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
// Configure multer to store files in memory // Configure multer to store files in memory
@ -35,11 +35,7 @@ export const upload = (req: Request, res: Response, next: NextFunction) => {
} }
for (const file of files) { for (const file of files) {
//check for correct filetypes //check for correct filetypes
if ( if (["jpeg", "png", "webp"].includes(file.mimetype)) {
file.mimetype !== "image/jpeg" &&
file.mimetype !== "image/png" &&
file.mimetype !== "image/webp"
) {
return res.status(StatusCodes.BAD_REQUEST).json({ return res.status(StatusCodes.BAD_REQUEST).json({
error: "Invalid file type", error: "Invalid file type",
details: [ details: [

View file

@ -0,0 +1,42 @@
import { Request, Response, NextFunction } from "express";
import multer from "multer";
import { StatusCodes } from "http-status-codes";
const multerInstance = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB
});
export const upload = (req: Request, res: Response, next: NextFunction) => {
multerInstance.single("image")(req, res, (err: any) => {
if (err) {
console.error(err);
return res
.status(StatusCodes.BAD_REQUEST)
.json({ error: err.name, details: [err.message] });
}
if (!req.file) {
return res.status(StatusCodes.BAD_REQUEST).json({
error: "No file uploaded",
details: [{ message: "Please upload a file" }],
});
}
const allowedTypes = ["image/jpeg", "image/png", "image/webp"];
if (!allowedTypes.includes(req.file.mimetype)) {
return res.status(StatusCodes.BAD_REQUEST).json({
error: "Invalid file type",
details: [
{
message:
"Only .jpeg, .png, or .webp files are allowed. Invalid: " +
req.file.originalname,
},
],
});
}
next();
});
};

View file

@ -0,0 +1,57 @@
import express from "express";
import { feed } from "../controllers/feedController";
const feedRouter = express.Router();
/**
* @swagger
* /api/feed:
* get:
* summary: Get a paginated feed of public posts
* description: Returns public posts sorted by creation date descending with cursor pagination.
* tags:
* - Feed
* parameters:
* - in: query
* name: createdAt
* schema:
* type: string
* format: date-time
* required: false
* description: Cursor for pagination, ISO timestamp of last post from previous page (only fetch posts created before this date)
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* required: false
* description: Number of posts to fetch
* responses:
* 200:
* description: List of posts with pagination cursor
* content:
* application/json:
* schema:
* type: object
* properties:
* posts:
* type: array
* items:
* type: object
* properties:
* id:
* type: string
* format: uuid
* createdAt:
* type: string
* format: date-time
* description:
* type: string
* nextCursor:
* type: string
* format: uuid
* nullable: true
* description: Cursor for the next page or null if no more posts
* 500:
* description: Server error
*/
feedRouter.get("/", feed);
export default feedRouter;

View file

@ -0,0 +1,50 @@
import express from "express";
import { authenticateToken } from "../middleware/authenticateToken";
import { followUser, unfollowUser } from "../controllers/followerController";
const followerRouter = express.Router();
/**
* @swagger
* /api/follower/follow/{username}:
* post:
* summary: follow a User
* tags: [User]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: username
* required: true
* schema:
* type: string
* description: Der Benutzername, nach dem gesucht werden soll
* responses:
* 200:
* description: Login erfolgreich
* 401:
* description: Ungültige Anmeldedaten
*/
followerRouter.post("/follow/:username", authenticateToken(), followUser);
/**
* @swagger
* /api/follower/unfollow/{username}:
* delete:
* summary: follow a User
* tags: [User]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: username
* required: true
* schema:
* type: string
* description: Der Benutzername, nach dem gesucht werden soll
* responses:
* 200:
* description: Login erfolgreich
* 401:
* description: Ungültige Anmeldedaten
*/
followerRouter.delete("/unfollow/:username", authenticateToken(), unfollowUser);
export default followerRouter;

View file

@ -2,12 +2,16 @@ import express from "express";
import { import {
getPost, getPost,
getUserPosts, getUserPosts,
like,
removeLike,
uploadPost as uploadPost, uploadPost as uploadPost,
} from "../controllers/postController"; } from "../controllers/postController";
import { upload } from "../middleware/fileUpload"; import { upload } from "../middleware/uploadMultiple";
import { validateData } from "../middleware/validationMiddleware"; import { validateData } from "../middleware/validationMiddleware";
import { uploadPostSchema } from "../schemas/postSchemas"; import { uploadPostSchema } from "../schemas/postSchemas";
import { authenticateToken } from "../middleware/authenticateToken";
import { optionalAuthenticateToken } from "../middleware/optionalAuthenticateToken";
const router = express.Router(); const router = express.Router();
/** /**
@ -16,7 +20,7 @@ const router = express.Router();
* post: * post:
* summary: Upload multiple images with metadata * summary: Upload multiple images with metadata
* tags: * tags:
* - posts * - Posts
* requestBody: * requestBody:
* required: true * required: true
* content: * content:
@ -57,13 +61,19 @@ const router = express.Router();
* 200: * 200:
* description: Images uploaded successfully * description: Images uploaded successfully
*/ */
router.post("/upload", upload, validateData(uploadPostSchema), uploadPost); router.post(
"/upload",
authenticateToken(),
upload,
validateData(uploadPostSchema),
uploadPost
);
/** /**
* @swagger * @swagger
* /api/posts/getPost/{postId}: * /api/posts/getPost/{postId}:
* get: * get:
* summary: Get Post * summary: Get Post
* tags: [posts] * tags: [Posts]
* security: * security:
* - bearerAuth: [] * - bearerAuth: []
* parameters: * parameters:
@ -76,23 +86,74 @@ router.post("/upload", upload, validateData(uploadPostSchema), uploadPost);
* responses: * responses:
* 200: * 200:
* description: Ok * description: Ok
* 401: * 404:
* description: not authenticated * description: not found
*/ */
router.get("/getPost/:postId", getPost); router.get("/getPost/:postId", optionalAuthenticateToken, getPost);
/** /**
* @swagger * @swagger
* /api/posts/getUserPosts/: * /api/posts/getUserPosts/{username}:
* get: * get:
* summary: Get Post * summary: Get Posts from user
* tags: [posts] * tags: [Posts]
* security: * parameters:
* - bearerAuth: [] * - in: path
* name: username
* required: true
* schema:
* type: string
* description: Der Benutzername, nach dem gesucht werden soll
* responses: * responses:
* 200: * 200:
* description: Ok * description: Ok
* 401: * 404:
* description: not authenticated * description: not found
*/ */
router.get("/getuserposts/", getUserPosts); router.get("/getuserposts/:username", optionalAuthenticateToken, getUserPosts);
/**
* @swagger
* /api/posts/like/{postId}:
* post:
* summary: follow a User
* tags: [Posts]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: postId
* required: true
* schema:
* type: string
* description: Der Benutzername, nach dem gesucht werden soll
* responses:
* 200:
* description: Login erfolgreich
* 401:
* description: Ungültige Anmeldedaten
*/
router.post("/like/:postId", authenticateToken(), like);
/**
* @swagger
* /api/posts/removeLike/{postId}:
* delete:
* summary: follow a User
* tags: [Posts]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: postId
* required: true
* schema:
* type: string
* description: Der Benutzername, nach dem gesucht werden soll
* responses:
* 200:
* description: Login erfolgreich
* 401:
* description: Ungültige Anmeldedaten
*/
router.delete("/removeLike/:postId", authenticateToken(), removeLike);
export default router; export default router;

View file

@ -0,0 +1,128 @@
import express from "express";
import { authenticateToken } from "../middleware/authenticateToken";
import {
getProfile,
getProfilePicture,
updateBio,
uploadProfilePicture,
} from "../controllers/profileController";
const profileRouter = express.Router();
import { upload } from "../middleware/uploadSingle";
import { updateBioSchema } from "../schemas/feedSchemas";
import { validateData } from "../middleware/validationMiddleware";
/**
* @swagger
* /api/profile/uploadProfilePicture:
* post:
* summary: Upload profile picture
* description: Uploads a profile picture for the current user
* tags:
* - Profile
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* image:
* type: string
* format: binary
* description: File to upload
* responses:
* 201:
* description: Image uploaded successfully
* 400:
* description: Invalid file or request
* 401:
* description: Unauthorized
*/
profileRouter.post(
"/uploadProfilePicture",
authenticateToken(),
upload,
uploadProfilePicture
);
/**
* @swagger
* /api/profile/getProfilePicture/{username}:
* get:
* summary: Get Profilepicture
* tags:
* - Profile
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: username
* required: true
* schema:
* type: string
* description: Der Benutzername, nach dem gesucht werden soll
* responses:
* 200:
* description: Ok
* 401:
* description: not authenticated
*/
profileRouter.get("/getProfilePicture/:username", getProfilePicture);
/**
* @swagger
* /api/profile/updateBio:
* post:
* summary: Update user bio
* description: Updates the bio (short description) of the currently authenticated user.
* tags:
* - Profile
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* bio:
* type: string
* description: New bio text for the user
* example: "When you program open source, you're programming COMMUNISM☭"
* responses:
* 200:
* description: Bio updated successfully
* 400:
* description: Invalid request
* 401:
* description: Unauthorized
*/
profileRouter.post(
"/updateBio",
authenticateToken(),
validateData(updateBioSchema),
updateBio
);
/**
* @swagger
* /api/profile/get/{username}:
* get:
* summary: Get user data
* tags: [Profile]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: username
* required: true
* schema:
* type: string
* description: Der Benutzername, nach dem gesucht werden soll
* responses:
* 200:
* description: Login erfolgreich
* 401:
* description: Ungültige Anmeldedaten
*/
profileRouter.get("/get/:username", getProfile);
export default profileRouter;

View file

@ -9,11 +9,7 @@ import { authenticateToken } from "../middleware/authenticateToken";
import { logout, refreshToken } from "../controllers/userController"; import { logout, refreshToken } from "../controllers/userController";
const userRouter = express.Router(); const userRouter = express.Router();
import { import { registerUser, loginUser } from "../controllers/userController";
registerUser,
loginUser,
getUser,
} from "../controllers/userController";
/** /**
* @swagger * @swagger
* components: * components:
@ -87,28 +83,7 @@ userRouter.post(
* description: Ungültige Anmeldedaten * description: Ungültige Anmeldedaten
*/ */
userRouter.post("/login", validateData(userLoginSchema), loginUser); userRouter.post("/login", validateData(userLoginSchema), loginUser);
/**
* @swagger
* /api/user/getUser/{username}:
* get:
* summary: Get user data
* tags: [User]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: username
* required: true
* schema:
* type: string
* description: Der Benutzername, nach dem gesucht werden soll
* responses:
* 200:
* description: Login erfolgreich
* 401:
* description: Ungültige Anmeldedaten
*/
userRouter.get("/getUser/:username", authenticateToken(), getUser);
/** /**
* @swagger * @swagger
* /api/user/refreshToken: * /api/user/refreshToken:
@ -162,4 +137,5 @@ userRouter.get("/refreshToken", refreshToken);
* description: not authenticated * description: not authenticated
*/ */
userRouter.delete("/logout", authenticateToken(), logout); userRouter.delete("/logout", authenticateToken(), logout);
export default userRouter; export default userRouter;

View file

@ -0,0 +1,15 @@
import { string, z } from "zod";
export const feedQuerySchema = z.object({
createdAt: z.string().datetime().optional(),
limit: z
.string()
.transform((val) => parseInt(val, 10))
.refine((num) => num > 0 && num <= 30, {
message: "Limit must be between 1 and 30",
})
.optional(),
});
export const updateBioSchema = z.object({
bio: z.string(),
});

View file

@ -3,7 +3,8 @@ import { Client } from "minio";
import dotenv from "dotenv"; import dotenv from "dotenv";
import userRouter from "./routes/userRoutes"; import userRouter from "./routes/userRoutes";
import postRouter from "./routes/postRoutes"; import postRouter from "./routes/postRoutes";
import { authenticateToken } from "./middleware/authenticateToken"; import profileRouter from "./routes/profileRoutes";
import followerRouter from "./routes/followerRoutes";
import bodyParser from "body-parser"; import bodyParser from "body-parser";
import cors from "cors"; import cors from "cors";
dotenv.config(); dotenv.config();
@ -30,6 +31,7 @@ export const minioClient = new Client({
import swaggerJSDoc from "swagger-jsdoc"; import swaggerJSDoc from "swagger-jsdoc";
import swaggerUi from "swagger-ui-express"; import swaggerUi from "swagger-ui-express";
import { deleteExpiredTokens } from "./tasks/deleteTokens"; import { deleteExpiredTokens } from "./tasks/deleteTokens";
import feedRouter from "./routes/feedRoutes";
const options = { const options = {
definition: { definition: {
@ -75,8 +77,10 @@ setInterval(
); );
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use("/api/user", userRouter); app.use("/api/user", userRouter);
app.use("/api/posts", authenticateToken(), postRouter); app.use("/api/posts", postRouter);
app.use("/api/profile", profileRouter);
app.use("/api/feed", feedRouter);
app.use("/api/follower/", followerRouter);
app.listen(port, () => { app.listen(port, () => {
console.log(`Server läuft auf http://localhost:${port}`); console.log(`Server läuft auf http://localhost:${port}`);
}); });

View file

@ -0,0 +1,10 @@
import { JwtPayload } from "../tokens";
export {};
declare global {
namespace Express {
interface Request {
user?: JwtPayload;
}
}
}

View file

@ -7,7 +7,8 @@
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"outDir": "./dist", "outDir": "./dist",
"resolveJsonModule": true // Allow importing JSON files "typeRoots": ["./src/types", "./node_modules/@types"],
"resolveJsonModule": true
}, },
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]

View file

@ -137,13 +137,6 @@
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
"@types/bcryptjs@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-3.0.0.tgz#d7be11653aa82cf17ffee4f3925f1f80cfc1add2"
integrity sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==
dependencies:
bcryptjs "*"
"@types/body-parser@*": "@types/body-parser@*":
version "1.19.5" version "1.19.5"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4"
@ -347,7 +340,7 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
bcryptjs@*, bcryptjs@^3.0.2: bcryptjs@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-3.0.2.tgz#caadcca1afefe372ed6e20f86db8e8546361c1ca" resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-3.0.2.tgz#caadcca1afefe372ed6e20f86db8e8546361c1ca"
integrity sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog== integrity sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==

View file

@ -31,7 +31,7 @@ function LoginAndSignUpPage({ signupProp }: { signupProp: boolean }) {
const toggleLogin = (event: React.MouseEvent<HTMLElement>) => { const toggleLogin = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault(); event.preventDefault();
setErrorMessages(undefined); setErrorMessages(undefined);
setSignup(!signup); signup ? navigate("/login") : navigate("/register");
}; };
const [formData, setFormData] = useState<FormData>({ const [formData, setFormData] = useState<FormData>({
username: "", username: "",