mirror of
https://github.com/bubblecup-12/VogelSocialMedia.git
synced 2025-07-08 13:38:47 +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
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 {
|
||||
const uploads = await Promise.all(
|
||||
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);
|
||||
const url = await minioClient.presignedGetObject(
|
||||
BUCKET,
|
||||
objectName,
|
||||
60 * 5 // 5 Minuten Gültigkeit
|
||||
60 * 10
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -76,10 +76,19 @@ 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({
|
||||
const user: JwtPayload | undefined = req.user;
|
||||
const postObject = await prisma.post.findFirst({
|
||||
// find the post by id
|
||||
where: {
|
||||
id: postId,
|
||||
...(user
|
||||
? {
|
||||
OR: [
|
||||
{ status: "PRIVATE", userId: user.id },
|
||||
{ status: "PUBLIC" },
|
||||
],
|
||||
}
|
||||
: { status: "PUBLIC" }),
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
|
@ -107,6 +116,20 @@ export const getPost = async (req: Request, res: Response) => {
|
|||
});
|
||||
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(
|
||||
// generate the presigned url for each image
|
||||
postObject?.media.map(async (image) => {
|
||||
|
@ -139,7 +162,9 @@ export const getPost = async (req: Request, res: Response) => {
|
|||
createdAt: postObject.createdAt,
|
||||
updatedAt: postObject.updatedAt,
|
||||
images: images.filter((image) => image !== null), // filter out the null images
|
||||
following: isFollowing,
|
||||
});
|
||||
return;
|
||||
} catch (err: any) {
|
||||
if (err.code === "NotFound") {
|
||||
// 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);
|
||||
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
|
||||
error: "Failed to retrieve post",
|
||||
details: [{ message: "Server error" }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// get all posts from a user
|
||||
export const getUserPosts = async (req: Request, res: Response) => {
|
||||
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({
|
||||
where: {
|
||||
userId: userId,
|
||||
user: {
|
||||
username: username, // hier greifst du auf die relationierte User-Tabelle zu
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!posts || posts.length === 0) {
|
||||
|
@ -192,3 +221,111 @@ export const getUserPosts = async (req: Request, res: Response) => {
|
|||
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" });
|
||||
};
|
||||
|
||||
// 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) => {
|
||||
const refreshToken: string | undefined = req.headers[
|
||||
"refresh-token"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue