init commit
This commit is contained in:
3
backend/.env
Normal file
3
backend/.env
Normal file
@ -0,0 +1,3 @@
|
||||
MONGO_URI=mongodb://localhost:27017/mern_resume_builder
|
||||
JWT_SECRET=9cd1f9e482ac014933a12ca44f3b1decfb2ee134ecd66203b4ad46a991cf216e69fd0058fb43741705724c32b31965d9416fc871ceafc48a624ab41dbd780905
|
||||
PORT=8000
|
||||
24
backend/.gitignore
vendored
Normal file
24
backend/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
13
backend/config/db.js
Normal file
13
backend/config/db.js
Normal file
@ -0,0 +1,13 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const connectDB = async () => {
|
||||
try {
|
||||
await mongoose.connect(process.env.MONGO_URI, {});
|
||||
console.log("MongoDB connected");
|
||||
} catch (err) {
|
||||
console.error("Error connecting to MongoDB", err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = connectDB;
|
||||
94
backend/controllers/authController.js
Normal file
94
backend/controllers/authController.js
Normal file
@ -0,0 +1,94 @@
|
||||
const User = require("../models/User");
|
||||
const bcrypt = require("bcryptjs");
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
// Generate JWT Token
|
||||
const generateToken = (userId) => {
|
||||
return jwt.sign({ id: userId }, process.env.JWT_SECRET, { expiresIn: "7d" });
|
||||
};
|
||||
|
||||
// @desc Register a new user
|
||||
// @route POST /api/auth/register
|
||||
// @access Public
|
||||
const registerUser = async (req, res) => {
|
||||
try {
|
||||
const { name, email, password, profileImageUrl } = req.body;
|
||||
|
||||
// Check if user already exists
|
||||
const userExists = await User.findOne({ email });
|
||||
if (userExists) {
|
||||
return res.status(400).json({ message: "User already exists" });
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(password, salt);
|
||||
|
||||
// Create new user
|
||||
const user = await User.create({
|
||||
name,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
profileImageUrl,
|
||||
});
|
||||
|
||||
// Return user data with JWT
|
||||
res.status(201).json({
|
||||
_id: user._id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
profileImageUrl: user.profileImageUrl,
|
||||
token: generateToken(user._id),
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Server error", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Login user
|
||||
// @route POST /api/auth/login
|
||||
// @access Public
|
||||
const loginUser = async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
const user = await User.findOne({ email });
|
||||
if (!user) {
|
||||
return res.status(500).json({ message: "Invalid email or password" });
|
||||
}
|
||||
|
||||
// Compare password
|
||||
const isMatch = await bcrypt.compare(password, user.password);
|
||||
if (!isMatch) {
|
||||
return res.status(500).json({ message: "Invalid email or password" });
|
||||
}
|
||||
|
||||
// Return user data with JWT
|
||||
res.json({
|
||||
_id: user._id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
profileImageUrl: user.profileImageUrl,
|
||||
token: generateToken(user._id),
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Server error", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get user profile
|
||||
// @route GET /api/auth/profile
|
||||
// @access Private (Requires JWT)
|
||||
const getUserProfile = async (req, res) => {
|
||||
try {
|
||||
const user = await User.findById(req.user.id).select("-password");
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: "User not found" });
|
||||
}
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Server error", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { registerUser, loginUser, getUserProfile };
|
||||
204
backend/controllers/resumeController.js
Normal file
204
backend/controllers/resumeController.js
Normal file
@ -0,0 +1,204 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const Resume = require("../models/Resume");
|
||||
|
||||
// @desc Create a new resume
|
||||
// @route POST /api/resumes
|
||||
// @access Private
|
||||
const createResume = async (req, res) => {
|
||||
try {
|
||||
const { title } = req.body;
|
||||
|
||||
// Default template
|
||||
const defaultResumeData = {
|
||||
profileInfo: {
|
||||
profileImg: null,
|
||||
previewUrl: "",
|
||||
fullName: "",
|
||||
designation: "",
|
||||
summary: "",
|
||||
},
|
||||
contactInfo: {
|
||||
email: "",
|
||||
phone: "",
|
||||
location: "",
|
||||
linkedin: "",
|
||||
github: "",
|
||||
website: "",
|
||||
},
|
||||
workExperience: [
|
||||
{
|
||||
company: "",
|
||||
role: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
education: [
|
||||
{
|
||||
degree: "",
|
||||
institution: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
},
|
||||
],
|
||||
skills: [
|
||||
{
|
||||
name: "",
|
||||
progress: 0,
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
title: "",
|
||||
description: "",
|
||||
github: "",
|
||||
liveDemo: "",
|
||||
},
|
||||
],
|
||||
certifications: [
|
||||
{
|
||||
title: "",
|
||||
issuer: "",
|
||||
year: "",
|
||||
},
|
||||
],
|
||||
languages: [
|
||||
{
|
||||
name: "",
|
||||
progress: 0,
|
||||
},
|
||||
],
|
||||
interests: [""],
|
||||
};
|
||||
|
||||
const newResume = await Resume.create({
|
||||
userId: req.user._id,
|
||||
title,
|
||||
...defaultResumeData,
|
||||
});
|
||||
|
||||
res.status(201).json(newResume);
|
||||
} catch (error) {
|
||||
res
|
||||
.status(500)
|
||||
.json({ message: "Failed to create resume", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get all resumes for logged-in user
|
||||
// @route GET /api/resumes
|
||||
// @access Private
|
||||
const getUserResumes = async (req, res) => {
|
||||
try {
|
||||
const resumes = await Resume.find({ userId: req.user._id }).sort({
|
||||
updatedAt: -1,
|
||||
});
|
||||
res.json(resumes);
|
||||
} catch (error) {
|
||||
res
|
||||
.status(500)
|
||||
.json({ message: "Failed to create resume", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get single resume by ID
|
||||
// @route GET /api/resumes/:id
|
||||
// @access Private
|
||||
const getResumeById = async (req, res) => {
|
||||
try {
|
||||
const resume = await Resume.findOne({ _id: req.params.id, userId: req.user._id });
|
||||
|
||||
if (!resume) {
|
||||
return res.status(404).json({ message: "Resume not found" });
|
||||
}
|
||||
|
||||
res.json(resume);
|
||||
} catch (error) {
|
||||
res
|
||||
.status(500)
|
||||
.json({ message: "Failed to create resume", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Update a resume
|
||||
// @route PUT /api/resumes/:id
|
||||
// @access Private
|
||||
const updateResume = async (req, res) => {
|
||||
try {
|
||||
const resume = await Resume.findOne({
|
||||
_id: req.params.id,
|
||||
userId: req.user._id,
|
||||
});
|
||||
|
||||
if (!resume) {
|
||||
return res.status(404).json({ message: "Resume not found or unauthorized" });
|
||||
}
|
||||
|
||||
// Merge updates from req.body into existing resume
|
||||
Object.assign(resume, req.body);
|
||||
|
||||
// Save updated resume
|
||||
const savedResume = await resume.save();
|
||||
|
||||
res.json(savedResume);
|
||||
} catch (error) {
|
||||
res
|
||||
.status(500)
|
||||
.json({ message: "Failed to create resume", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Delete a resume
|
||||
// @route DELETE /api/resumes/:id
|
||||
// @access Private
|
||||
const deleteResume = async (req, res) => {
|
||||
try {
|
||||
const resume = await Resume.findOne({
|
||||
_id: req.params.id,
|
||||
userId: req.user._id,
|
||||
});
|
||||
|
||||
if (!resume) {
|
||||
return res.status(404).json({ message: "Resume not found or unauthorized" });
|
||||
}
|
||||
|
||||
// Delete thumbnailLink and profilePreviewUrl images from uploads folder
|
||||
const uploadsFolder = path.join(__dirname, '..', 'uploads');
|
||||
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
||||
|
||||
if(resume.thumbnailLink){
|
||||
const oldThumbnail = path.join(uploadsFolder, path.basename(resume.thumbnailLink));
|
||||
if (fs.existsSync(oldThumbnail)) fs.unlinkSync(oldThumbnail);
|
||||
}
|
||||
|
||||
if(resume.profileInfo?.profilePreviewUrl){
|
||||
const oldProfile = path.join(uploadsFolder, path.basename(resume.profileInfo.profilePreviewUrl));
|
||||
if (fs.existsSync(oldProfile)) fs.unlinkSync(oldProfile);
|
||||
}
|
||||
|
||||
const deleted = await Resume.findOneAndDelete({
|
||||
_id: req.params.id,
|
||||
userId: req.user._id,
|
||||
});
|
||||
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ message: "Resume not found or unauthorized" });
|
||||
}
|
||||
|
||||
res.json({ message: "Resume deleted successfully" });
|
||||
} catch (error) {
|
||||
res
|
||||
.status(500)
|
||||
.json({ message: "Failed to create resume", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createResume,
|
||||
getUserResumes,
|
||||
getResumeById,
|
||||
updateResume,
|
||||
deleteResume,
|
||||
};
|
||||
58
backend/controllers/uploadImages.js
Normal file
58
backend/controllers/uploadImages.js
Normal file
@ -0,0 +1,58 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Resume = require("../models/Resume");
|
||||
const upload = require("../middlewares/uploadMiddleware");
|
||||
|
||||
const uploadResumeImages = async (req, res) => {
|
||||
try {
|
||||
upload.fields([{ name: 'thumbnail' }, { name: 'profileImage' }])(req, res, async (err) => {
|
||||
if (err) {
|
||||
return res.status(400).json({ message: "File upload failed", error: err.message });
|
||||
}
|
||||
|
||||
const resumeId = req.params.id;
|
||||
const resume = await Resume.findOne({ _id: resumeId, userId: req.user._id });
|
||||
|
||||
if (!resume) {
|
||||
return res.status(404).json({ message: "Resume not found or unauthorized" });
|
||||
}
|
||||
|
||||
const uploadsFolder = path.join(__dirname, '..', 'uploads');
|
||||
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
||||
|
||||
const newThumbnail = req.files.thumbnail?.[0];
|
||||
const newProfileImage = req.files.profileImage?.[0];
|
||||
|
||||
// If new thumbnail uploaded, delete old one
|
||||
if (newThumbnail) {
|
||||
if(resume.thumbnailLink){
|
||||
const oldThumbnail = path.join(uploadsFolder, path.basename(resume.thumbnailLink));
|
||||
if (fs.existsSync(oldThumbnail)) fs.unlinkSync(oldThumbnail);
|
||||
}
|
||||
resume.thumbnailLink = `${baseUrl}/uploads/${newThumbnail.filename}`;
|
||||
}
|
||||
|
||||
// If new profile image uploaded, delete old one
|
||||
if (newProfileImage) {
|
||||
if(resume.profileInfo?.profilePreviewUrl){
|
||||
const oldProfile = path.join(uploadsFolder, path.basename(resume.profileInfo.profilePreviewUrl));
|
||||
if (fs.existsSync(oldProfile)) fs.unlinkSync(oldProfile);
|
||||
}
|
||||
resume.profileInfo.profilePreviewUrl = `${baseUrl}/uploads/${newProfileImage.filename}`;
|
||||
}
|
||||
|
||||
await resume.save();
|
||||
|
||||
res.status(200).json({
|
||||
message: "Images uploaded successfully",
|
||||
thumbnailLink: resume.thumbnailLink,
|
||||
profilePreviewUrl: resume.profileInfo.profilePreviewUrl,
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error uploading images:", err);
|
||||
res.status(500).json({ message: "Failed to upload images", error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { uploadResumeImages };
|
||||
22
backend/middlewares/authMiddleware.js
Normal file
22
backend/middlewares/authMiddleware.js
Normal file
@ -0,0 +1,22 @@
|
||||
const jwt = require("jsonwebtoken");
|
||||
const User = require("../models/User");
|
||||
|
||||
// Middleware to protect routes
|
||||
const protect = async (req, res, next) => {
|
||||
try {
|
||||
let token = req.headers.authorization;
|
||||
|
||||
if (token && token.startsWith("Bearer")) {
|
||||
token = token.split(" ")[1]; // Extract token
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = await User.findById(decoded.id).select("-password");
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ message: "Not authorized, no token" });
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(401).json({ message: "Token failed", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { protect };
|
||||
25
backend/middlewares/uploadMiddleware.js
Normal file
25
backend/middlewares/uploadMiddleware.js
Normal file
@ -0,0 +1,25 @@
|
||||
const multer = require('multer');
|
||||
|
||||
// Configure storage
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, 'uploads/');
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
cb(null, `${Date.now()}-${file.originalname}`);
|
||||
},
|
||||
});
|
||||
|
||||
// File filter
|
||||
const fileFilter = (req, file, cb) => {
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg'];
|
||||
if (allowedTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only .jpeg, .jpg and .png formats are allowed'), false);
|
||||
}
|
||||
};
|
||||
|
||||
const upload = multer({ storage, fileFilter });
|
||||
|
||||
module.exports = upload;
|
||||
86
backend/models/Resume.js
Normal file
86
backend/models/Resume.js
Normal file
@ -0,0 +1,86 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const ResumeSchema = new mongoose.Schema(
|
||||
{
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
thumbnailLink: {
|
||||
type: String,
|
||||
},
|
||||
template:{
|
||||
theme: String,
|
||||
colorPalette: [String]
|
||||
},
|
||||
profileInfo: {
|
||||
profilePreviewUrl: String,
|
||||
fullName: String,
|
||||
designation: String,
|
||||
summary: String,
|
||||
},
|
||||
contactInfo: {
|
||||
email: String,
|
||||
phone: String,
|
||||
location: String,
|
||||
linkedin: String,
|
||||
github: String,
|
||||
website: String,
|
||||
},
|
||||
workExperience: [
|
||||
{
|
||||
company: String,
|
||||
role: String,
|
||||
startDate: String,
|
||||
endDate: String,
|
||||
description: String,
|
||||
},
|
||||
],
|
||||
education: [
|
||||
{
|
||||
degree: String,
|
||||
institution: String,
|
||||
startDate: String,
|
||||
endDate: String,
|
||||
},
|
||||
],
|
||||
skills: [
|
||||
{
|
||||
name: String,
|
||||
progress: Number,
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
title: String,
|
||||
description: String,
|
||||
github: String,
|
||||
liveDemo: String,
|
||||
},
|
||||
],
|
||||
certifications: [
|
||||
{
|
||||
title: String,
|
||||
issuer: String,
|
||||
year: String,
|
||||
},
|
||||
],
|
||||
languages: [
|
||||
{
|
||||
name: String,
|
||||
progress: Number,
|
||||
},
|
||||
],
|
||||
interests: [String],
|
||||
},
|
||||
{
|
||||
timestamps: { createdAt: "createdAt", updatedAt: "updatedAt" },
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("Resume", ResumeSchema);
|
||||
13
backend/models/User.js
Normal file
13
backend/models/User.js
Normal file
@ -0,0 +1,13 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const UserSchema = new mongoose.Schema(
|
||||
{
|
||||
name: { type: String, required: true },
|
||||
email: { type: String, required: true, unique: true },
|
||||
password: { type: String, required: true },
|
||||
profileImageUrl: { type: String, default: null },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("User", UserSchema);
|
||||
1723
backend/package-lock.json
generated
Normal file
1723
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
backend/package.json
Normal file
26
backend/package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "resume-builder",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.5.0",
|
||||
"express": "^5.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongoose": "^8.13.2",
|
||||
"multer": "^1.4.5-lts.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.9"
|
||||
}
|
||||
}
|
||||
23
backend/routes/authRoutes.js
Normal file
23
backend/routes/authRoutes.js
Normal file
@ -0,0 +1,23 @@
|
||||
const express = require("express");
|
||||
const { registerUser, loginUser, getUserProfile } = require("../controllers/authController");
|
||||
const { protect } = require("../middlewares/authMiddleware");
|
||||
const upload = require('../middlewares/uploadMiddleware')
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Auth Routes
|
||||
router.post("/register", registerUser); // Register User
|
||||
router.post("/login", loginUser); // Login User
|
||||
router.get("/profile", protect, getUserProfile); // Get User Profile
|
||||
|
||||
router.post("/upload-image", upload.single("image"), (req, res) => {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ message: "No file uploaded" });
|
||||
}
|
||||
const imageUrl = `${req.protocol}://${req.get("host")}/uploads/${
|
||||
req.file.filename
|
||||
}`;
|
||||
res.status(200).json({ imageUrl });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
23
backend/routes/resumeRoutes.js
Normal file
23
backend/routes/resumeRoutes.js
Normal file
@ -0,0 +1,23 @@
|
||||
const express = require("express");
|
||||
const {
|
||||
createResume,
|
||||
getUserResumes,
|
||||
getResumeById,
|
||||
updateResume,
|
||||
deleteResume,
|
||||
} = require("../controllers/resumeController");
|
||||
const { protect } = require("../middlewares/authMiddleware");
|
||||
const { uploadResumeImages } = require("../controllers/uploadImages");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
||||
router.post("/", protect, createResume); // Create Resume
|
||||
router.get("/", protect, getUserResumes); // Get Resume
|
||||
router.get("/:id", protect, getResumeById); // Get Resume By ID
|
||||
router.put("/:id", protect, updateResume); // Update Resume
|
||||
router.put("/:id/upload-images", protect, uploadResumeImages);
|
||||
|
||||
router.delete("/:id", protect, deleteResume); // Delete Resume
|
||||
|
||||
module.exports = router;
|
||||
45
backend/server.js
Normal file
45
backend/server.js
Normal file
@ -0,0 +1,45 @@
|
||||
require("dotenv").config();
|
||||
const express = require("express");
|
||||
const cors = require("cors");
|
||||
const path = require("path");
|
||||
const connectDB = require('./config/db')
|
||||
|
||||
const authRoutes = require('./routes/authRoutes')
|
||||
const resumeRoutes = require('./routes/resumeRoutes')
|
||||
|
||||
const app = express();
|
||||
|
||||
// Middleware to handle CORS
|
||||
app.use(
|
||||
cors({
|
||||
origin: "*",
|
||||
methods: ["GET", "POST", "PUT", "DELETE"],
|
||||
allowedHeaders: ["Content-Type", "Authorization"],
|
||||
})
|
||||
);
|
||||
|
||||
// Connect Database
|
||||
connectDB();
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
|
||||
|
||||
// Routes
|
||||
app.use("/api/auth", authRoutes);
|
||||
app.use("/api/resume", resumeRoutes);
|
||||
|
||||
// Serve uploads folder
|
||||
app.use(
|
||||
"/uploads",
|
||||
express.static(path.join(__dirname, "uploads"), {
|
||||
setHeaders: (res, path) => {
|
||||
res.set("Access-Control-Allow-Origin", "http://localhost:5173");
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
// Start Server
|
||||
const PORT = process.env.PORT || 5000;
|
||||
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
|
||||
BIN
backend/uploads/1751829475177-image (1) (1).png
Normal file
BIN
backend/uploads/1751829475177-image (1) (1).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
Reference in New Issue
Block a user