mirror of
https://github.com/bubblecup-12/VogelSocialMedia.git
synced 2025-07-06 15:18:48 +00:00
added endpoints for profile and feed
This commit is contained in:
parent
9e6eeb27fd
commit
1800056918
22 changed files with 952 additions and 110 deletions
|
@ -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",
|
||||||
|
|
|
@ -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])
|
||||||
|
|
31
code/backend/src/controllers/feedController.ts
Normal file
31
code/backend/src/controllers/feedController.ts
Normal 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" });
|
||||||
|
}
|
||||||
|
};
|
124
code/backend/src/controllers/followerController.ts
Normal file
124
code/backend/src/controllers/followerController.ts
Normal 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" }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -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" }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
235
code/backend/src/controllers/profileController.ts
Normal file
235
code/backend/src/controllers/profileController.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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"
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
25
code/backend/src/middleware/optionalAuthenticateToken.ts
Normal file
25
code/backend/src/middleware/optionalAuthenticateToken.ts
Normal 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();
|
||||||
|
});
|
||||||
|
};
|
|
@ -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: [
|
42
code/backend/src/middleware/uploadSingle.ts
Normal file
42
code/backend/src/middleware/uploadSingle.ts
Normal 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();
|
||||||
|
});
|
||||||
|
};
|
57
code/backend/src/routes/feedRoutes.ts
Normal file
57
code/backend/src/routes/feedRoutes.ts
Normal 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;
|
50
code/backend/src/routes/followerRoutes.ts
Normal file
50
code/backend/src/routes/followerRoutes.ts
Normal 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;
|
|
@ -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;
|
||||||
|
|
128
code/backend/src/routes/profileRoutes.ts
Normal file
128
code/backend/src/routes/profileRoutes.ts
Normal 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;
|
|
@ -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;
|
||||||
|
|
15
code/backend/src/schemas/feedSchemas.ts
Normal file
15
code/backend/src/schemas/feedSchemas.ts
Normal 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(),
|
||||||
|
});
|
|
@ -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}`);
|
||||||
});
|
});
|
||||||
|
|
10
code/backend/src/types/express/index.d.ts
vendored
Normal file
10
code/backend/src/types/express/index.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { JwtPayload } from "../tokens";
|
||||||
|
|
||||||
|
export {};
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
user?: JwtPayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"]
|
||||||
|
|
|
@ -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==
|
||||||
|
|
|
@ -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: "",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue