aboutsummaryrefslogtreecommitdiff
path: root/src/app/api
diff options
context:
space:
mode:
authorschererleander <leander@schererleander.de>2025-07-02 22:18:21 +0200
committerschererleander <leander@schererleander.de>2025-07-02 22:18:21 +0200
commitaefea182eefc5fe6b4a69860db5a7be471cf6679 (patch)
tree92b09aff64fe7e78a07abbb1d5bf5c455c69daaa /src/app/api
parentc12ca8a52d27b1931d826df10119984f2a7c58dd (diff)
feat: add user management API
Diffstat (limited to 'src/app/api')
-rw-r--r--src/app/api/user/password/route.ts76
-rw-r--r--src/app/api/user/profile-image/route.ts199
-rw-r--r--src/app/api/user/profile/route.ts78
3 files changed, 353 insertions, 0 deletions
diff --git a/src/app/api/user/password/route.ts b/src/app/api/user/password/route.ts
new file mode 100644
index 0000000..9972fb5
--- /dev/null
+++ b/src/app/api/user/password/route.ts
@@ -0,0 +1,76 @@
+import { NextRequest, NextResponse } from "next/server"
+import { getServerSession } from "next-auth/next"
+import bcrypt from "bcryptjs"
+import dbConnect from "@/lib/mongodb"
+import User from "@/model/User"
+import { authOptions } from "@/lib/auth"
+import { updatePasswordSchema } from "@/lib/validation"
+
+export async function PATCH(request: NextRequest) {
+ try {
+ const session = await getServerSession(authOptions)
+
+ if (!session?.user?.id) {
+ return NextResponse.json(
+ { error: "Unauthorized" },
+ { status: 401 }
+ )
+ }
+
+ const body = await request.json()
+
+ const result = updatePasswordSchema.safeParse(body)
+
+ if (!result.success) {
+ return NextResponse.json(
+ { error: "Validation failed", details: result.error.errors },
+ { status: 400 }
+ )
+ }
+
+ const { currentPassword, newPassword } = result.data
+
+ await dbConnect()
+
+ // Get user with current password
+ const user = await User.findById(session.user.id)
+
+ if (!user) {
+ return NextResponse.json(
+ { error: "User not found" },
+ { status: 404 }
+ )
+ }
+
+ // Verify current password
+ const isCurrentPasswordValid = await bcrypt.compare(currentPassword, user.password)
+
+ if (!isCurrentPasswordValid) {
+ return NextResponse.json(
+ { error: "Current password is incorrect" },
+ { status: 400 }
+ )
+ }
+
+ // Hash new password
+ const hashedNewPassword = await bcrypt.hash(newPassword, 12)
+
+ // Update password
+ await User.findByIdAndUpdate(
+ session.user.id,
+ { password: hashedNewPassword }
+ )
+
+ return NextResponse.json({
+ message: "Password updated successfully"
+ })
+
+ } catch (error) {
+ console.error("Password update error:", error)
+
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 }
+ )
+ }
+} \ No newline at end of file
diff --git a/src/app/api/user/profile-image/route.ts b/src/app/api/user/profile-image/route.ts
new file mode 100644
index 0000000..aabc337
--- /dev/null
+++ b/src/app/api/user/profile-image/route.ts
@@ -0,0 +1,199 @@
+import { NextRequest, NextResponse } from "next/server"
+import { getServerSession } from "next-auth/next"
+import sharp from "sharp"
+import { authOptions } from "@/lib/auth"
+import dbConnect from "@/lib/mongodb"
+import User from "@/model/User"
+import { uploadToMinio, deleteFromMinio } from "@/lib/minio"
+
+// Configuration
+const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
+const ALLOWED_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif']
+const OUTPUT_WIDTH = 400
+const OUTPUT_HEIGHT = 400
+const OUTPUT_QUALITY = 80
+
+export async function POST(request: NextRequest) {
+ try {
+ // Check authentication
+ const session = await getServerSession(authOptions)
+ if (!session?.user?.id) {
+ return NextResponse.json(
+ { error: "Unauthorized" },
+ { status: 401 }
+ )
+ }
+
+ // Parse FormData
+ const formData = await request.formData()
+ const file = formData.get('image') as File
+
+ // Validate file exists
+ if (!file) {
+ return NextResponse.json(
+ { error: "No file provided" },
+ { status: 400 }
+ )
+ }
+
+ // Validate file type
+ if (!ALLOWED_TYPES.includes(file.type)) {
+ return NextResponse.json(
+ { error: `Invalid file type. Allowed: ${ALLOWED_TYPES.join(', ')}` },
+ { status: 400 }
+ )
+ }
+
+ // Validate file size
+ if (file.size > MAX_FILE_SIZE) {
+ return NextResponse.json(
+ { error: `File too large. Maximum size: ${MAX_FILE_SIZE / 1024 / 1024}MB` },
+ { status: 400 }
+ )
+ }
+
+ // Convert File to Buffer
+ const buffer = Buffer.from(await file.arrayBuffer())
+
+ // Validate image and get metadata
+ let imageMetadata
+ try {
+ imageMetadata = await sharp(buffer).metadata()
+ } catch (error) {
+ return NextResponse.json(
+ { error: "Invalid image file" },
+ { status: 400 }
+ )
+ }
+
+ // Additional validation
+ if (!imageMetadata.width || !imageMetadata.height) {
+ return NextResponse.json(
+ { error: "Unable to read image dimensions" },
+ { status: 400 }
+ )
+ }
+
+ // Process image: resize and convert to WebP
+ const processedBuffer = await sharp(buffer)
+ .resize(OUTPUT_WIDTH, OUTPUT_HEIGHT, {
+ fit: 'cover',
+ position: 'center'
+ })
+ .webp({ quality: OUTPUT_QUALITY })
+ .toBuffer()
+
+ // Generate unique filename
+ const timestamp = Date.now()
+ const filename = `avatar_${timestamp}.webp`
+ const key = `users/${session.user.id}/profile/${filename}`
+
+ // Connect to database
+ await dbConnect()
+
+ // Get current user to check for existing profile image
+ const currentUser = await User.findById(session.user.id)
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: "User not found" },
+ { status: 404 }
+ )
+ }
+
+ // Delete old profile image from MinIO if it exists
+ if (currentUser.profileImage?.key) {
+ try {
+ await deleteFromMinio(currentUser.profileImage.key)
+ } catch (error) {
+ console.warn("Failed to delete old profile image:", error)
+ // Continue with upload even if deletion fails
+ }
+ }
+
+ // Upload to MinIO
+ const minioUrl = await uploadToMinio(key, processedBuffer, 'image/webp')
+
+ // Update user with new profile image
+ const updatedUser = await User.findByIdAndUpdate(
+ session.user.id,
+ {
+ profileImage: {
+ url: minioUrl,
+ key: key,
+ uploadedAt: new Date()
+ }
+ },
+ { new: true }
+ )
+
+ // Return success response
+ return NextResponse.json({
+ message: "Profile image uploaded successfully",
+ profileImage: {
+ url: minioUrl,
+ uploadedAt: new Date()
+ }
+ }, { status: 200 })
+
+ } catch (error) {
+ console.error("Profile image upload error:", error)
+
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 }
+ )
+ }
+}
+
+export async function DELETE(request: NextRequest) {
+ try {
+ // Check authentication
+ const session = await getServerSession(authOptions)
+ if (!session?.user?.id) {
+ return NextResponse.json(
+ { error: "Unauthorized" },
+ { status: 401 }
+ )
+ }
+
+ await dbConnect()
+
+ // Get current user
+ const currentUser = await User.findById(session.user.id)
+ if (!currentUser) {
+ return NextResponse.json(
+ { error: "User not found" },
+ { status: 404 }
+ )
+ }
+
+ // Check if user has profile image
+ if (!currentUser.profileImage?.key) {
+ return NextResponse.json(
+ { error: "No profile image to delete" },
+ { status: 400 }
+ )
+ }
+
+ // Delete from MinIO
+ await deleteFromMinio(currentUser.profileImage.key)
+
+ // Remove profile image from user document
+ await User.findByIdAndUpdate(
+ session.user.id,
+ { $unset: { profileImage: 1 } }
+ )
+
+ return NextResponse.json({
+ message: "Profile image deleted successfully"
+ }, { status: 200 })
+
+ } catch (error) {
+ console.error("Profile image deletion error:", error)
+
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 }
+ )
+ }
+} \ No newline at end of file
diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts
new file mode 100644
index 0000000..0cac7a3
--- /dev/null
+++ b/src/app/api/user/profile/route.ts
@@ -0,0 +1,78 @@
+import { NextRequest, NextResponse } from "next/server"
+import { getServerSession } from "next-auth/next"
+import dbConnect from "@/lib/mongodb"
+import User from "@/model/User"
+import { authOptions } from "@/lib/auth"
+import { updateProfileSchema } from "@/lib/validation"
+
+export async function PATCH(request: NextRequest) {
+ try {
+ const session = await getServerSession(authOptions)
+
+ if (!session?.user?.id) {
+ return NextResponse.json(
+ { error: "Unauthorized" },
+ { status: 401 }
+ )
+ }
+
+ const body = await request.json()
+
+ const result = updateProfileSchema.safeParse(body)
+
+ if (!result.success) {
+ return NextResponse.json(
+ { error: "Validation failed", details: result.error.errors },
+ { status: 400 }
+ )
+ }
+
+ const { name, email } = result.data
+
+ await dbConnect()
+
+ // Check if email is already taken by another user
+ const existingUser = await User.findOne({
+ email,
+ _id: { $ne: session.user.id }
+ })
+
+ if (existingUser) {
+ return NextResponse.json(
+ { error: "Email is already in use" },
+ { status: 409 }
+ )
+ }
+
+ // Update user
+ const updatedUser = await User.findByIdAndUpdate(
+ session.user.id,
+ { name, email },
+ { new: true }
+ )
+
+ if (!updatedUser) {
+ return NextResponse.json(
+ { error: "User not found" },
+ { status: 404 }
+ )
+ }
+
+ return NextResponse.json({
+ message: "Profile updated successfully",
+ user: {
+ id: updatedUser._id,
+ name: updatedUser.name,
+ email: updatedUser.email,
+ }
+ })
+
+ } catch (error) {
+ console.error("Profile update error:", error)
+
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 }
+ )
+ }
+} \ No newline at end of file