mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-08 21:10:12 +02:00
Add user profile image
This commit is contained in:
@@ -35,7 +35,11 @@ const updateUser = async (
|
||||
res: Response
|
||||
) => {
|
||||
const { userId } = req.params;
|
||||
const user = await service.users.updateUser({ userId }, req.body);
|
||||
|
||||
const user = await service.users.updateUser(
|
||||
{ userId },
|
||||
{ ...req.body, image: req.file }
|
||||
);
|
||||
const success = ApiSuccess.ok({ data: toApiModel.users([user])[0] });
|
||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
Artist,
|
||||
ArtistRating,
|
||||
External,
|
||||
File,
|
||||
FileType,
|
||||
Genre,
|
||||
Image,
|
||||
ImageType,
|
||||
@@ -585,9 +587,21 @@ const relatedServerPermissions = (items: ServerPermission[]) => {
|
||||
});
|
||||
};
|
||||
|
||||
const relatedFile = (item: File) => {
|
||||
return {
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||
id: item.id,
|
||||
name: item.fileName,
|
||||
path: item.path,
|
||||
type: item.type,
|
||||
/* eslint-enable sort-keys-fix/sort-keys-fix */
|
||||
};
|
||||
};
|
||||
|
||||
const users = (
|
||||
items: (User & {
|
||||
accessToken?: string;
|
||||
files?: File[];
|
||||
refreshToken?: string;
|
||||
serverFolderPermissions?: ServerFolderPermission[];
|
||||
serverPermissions?: ServerPermission[];
|
||||
@@ -595,11 +609,14 @@ const users = (
|
||||
) => {
|
||||
return (
|
||||
items.map((item) => {
|
||||
const avatar = item.files?.find((f) => f.type === FileType.USER);
|
||||
|
||||
return {
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||
id: item.id,
|
||||
username: item.username,
|
||||
displayName: item.displayName,
|
||||
avatar: avatar ? relatedFile(avatar) : null,
|
||||
accessToken: item.accessToken,
|
||||
refreshToken: item.refreshToken,
|
||||
enabled: item.enabled,
|
||||
|
||||
Generated
+1009
-31
File diff suppressed because it is too large
Load Diff
@@ -34,9 +34,11 @@
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"@types/md5": "^2.3.2",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^18.8.4",
|
||||
"@types/passport-jwt": "^3.0.7",
|
||||
"@types/passport-local": "^1.0.34",
|
||||
"@types/sharp": "^0.31.0",
|
||||
"@typescript-eslint/parser": "^5.40.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
@@ -69,10 +71,12 @@
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"md5": "^2.3.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"p-throttle": "^4.1.1",
|
||||
"passport": "^0.4.1",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"sharp": "^0.31.2",
|
||||
"socket.io": "^4.5.3",
|
||||
"zod": "^3.19.1"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "FileType" AS ENUM ('ALBUM', 'SONG', 'AUDIO', 'USER');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "File" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"path" TEXT NOT NULL,
|
||||
"originalName" TEXT NOT NULL,
|
||||
"fileName" TEXT NOT NULL,
|
||||
"size" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"type" "FileType" NOT NULL,
|
||||
"userId" UUID,
|
||||
|
||||
CONSTRAINT "File_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "File_path_key" ON "File"("path");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "File_fileName_key" ON "File"("fileName");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "File_userId_type_key" ON "File"("userId", "type");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "File" ADD CONSTRAINT "File_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -48,6 +48,13 @@ enum TaskType {
|
||||
LASTFM
|
||||
}
|
||||
|
||||
enum FileType {
|
||||
ALBUM
|
||||
SONG
|
||||
AUDIO
|
||||
USER
|
||||
}
|
||||
|
||||
model RefreshToken {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
token String @unique
|
||||
@@ -74,6 +81,7 @@ model User {
|
||||
albumRatings AlbumRating[]
|
||||
songRatings SongRating[]
|
||||
refreshTokens RefreshToken[]
|
||||
files File[]
|
||||
|
||||
serverFolderPermissions ServerFolderPermission[]
|
||||
serverPermissions ServerPermission[]
|
||||
@@ -85,6 +93,22 @@ model User {
|
||||
tasks Task[]
|
||||
}
|
||||
|
||||
model File {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
path String @unique
|
||||
originalName String
|
||||
fileName String @unique
|
||||
size Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
type FileType
|
||||
|
||||
User User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String? @db.Uuid
|
||||
|
||||
@@unique(fields: [userId, type], name: "uniqueFileId")
|
||||
}
|
||||
|
||||
model History {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import express, { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import { controller } from '@controllers/index';
|
||||
import { service } from '@services/index';
|
||||
import { ApiError } from '@utils/index';
|
||||
import { validation } from '@validations/index';
|
||||
import { validateRequest } from '@validations/shared.validation';
|
||||
import { authenticateAdmin } from '../middleware/authenticate-admin';
|
||||
const storage = multer.memoryStorage();
|
||||
const upload = multer({ storage: storage });
|
||||
|
||||
export const router: Router = express.Router({ mergeParams: true });
|
||||
|
||||
@@ -41,6 +44,7 @@ router
|
||||
.get(validateRequest(validation.users.detail), controller.users.getUserDetail)
|
||||
.patch(
|
||||
validateRequest(validation.users.updateUser),
|
||||
upload.single('image'),
|
||||
controller.users.updateUser
|
||||
)
|
||||
.delete(
|
||||
|
||||
@@ -17,8 +17,10 @@ const PORT = 9321;
|
||||
|
||||
const app = express();
|
||||
const staticPath = path.join(__dirname, '../feishin-client/');
|
||||
const filesPath = path.join(__dirname, './files/');
|
||||
|
||||
app.use(express.static(staticPath));
|
||||
app.use('/files', express.static(filesPath));
|
||||
app.use(
|
||||
cors({
|
||||
credentials: false,
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import fs from 'fs';
|
||||
import { FileType } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import md5 from 'md5';
|
||||
import sharp from 'sharp';
|
||||
import { prisma } from '@lib/prisma';
|
||||
import { AuthUser } from '@middleware/authenticate';
|
||||
import { randomString, ApiError } from '@utils/index';
|
||||
import { SortOrder } from '../types/types';
|
||||
|
||||
const findById = async (user: AuthUser, options: { id: string }) => {
|
||||
const { id } = options;
|
||||
@@ -11,7 +16,11 @@ const findById = async (user: AuthUser, options: { id: string }) => {
|
||||
}
|
||||
|
||||
const uniqueUser = await prisma.user.findUnique({
|
||||
include: { serverFolderPermissions: true, serverPermissions: true },
|
||||
include: {
|
||||
files: true,
|
||||
serverFolderPermissions: true,
|
||||
serverPermissions: true,
|
||||
},
|
||||
where: { id },
|
||||
});
|
||||
|
||||
@@ -23,20 +32,23 @@ const findById = async (user: AuthUser, options: { id: string }) => {
|
||||
};
|
||||
|
||||
const findMany = async () => {
|
||||
const users = await prisma.user.findMany({});
|
||||
const users = await prisma.user.findMany({
|
||||
include: { files: true },
|
||||
orderBy: [{ isAdmin: SortOrder.DESC }, { username: SortOrder.ASC }],
|
||||
});
|
||||
return users;
|
||||
};
|
||||
|
||||
const createUser = async (
|
||||
user: AuthUser,
|
||||
options: {
|
||||
data: {
|
||||
displayName?: string;
|
||||
isAdmin?: boolean;
|
||||
password: string;
|
||||
username: string;
|
||||
}
|
||||
) => {
|
||||
const { password, username, displayName, isAdmin } = options;
|
||||
const { password, username, displayName, isAdmin } = data;
|
||||
|
||||
if (isAdmin && !user.isSuperAdmin) {
|
||||
throw ApiError.badRequest('You are not authorized to create an admin.');
|
||||
@@ -91,18 +103,81 @@ const updateUser = async (
|
||||
options: { userId: string },
|
||||
data: {
|
||||
displayName?: string;
|
||||
image?: Express.Multer.File | null;
|
||||
isAdmin?: boolean;
|
||||
password?: string;
|
||||
username?: string;
|
||||
}
|
||||
) => {
|
||||
const { userId } = options;
|
||||
const { username, password, isAdmin, displayName } = data;
|
||||
const { username, password, isAdmin, displayName, image } = data;
|
||||
|
||||
const hashedPassword = password && (await bcrypt.hash(password, 12));
|
||||
|
||||
let avatar: {
|
||||
fileName: string;
|
||||
path: string;
|
||||
size: number;
|
||||
type: FileType;
|
||||
} | null = null;
|
||||
|
||||
if (image) {
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
include: { files: true },
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
const existingFile = existingUser?.files.find(
|
||||
(file) => file.type === FileType.USER
|
||||
);
|
||||
|
||||
// Delete the existing file
|
||||
if (existingFile) {
|
||||
await prisma.file.delete({ where: { id: existingFile.id } });
|
||||
const filePath = `../files/${existingFile.fileName}`;
|
||||
fs.unlink(filePath, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
// Create optimized webp image and delete the original
|
||||
const avatarFilename = `${md5(randomString(12))}.webp`;
|
||||
const avatarPath = `files/${avatarFilename}`;
|
||||
const newImage = await sharp(image.buffer)
|
||||
.webp({ quality: 20 })
|
||||
.toFile(avatarPath);
|
||||
avatar = {
|
||||
fileName: avatarFilename,
|
||||
path: avatarPath,
|
||||
size: newImage.size,
|
||||
type: FileType.USER,
|
||||
};
|
||||
}
|
||||
|
||||
const user = await prisma.user.update({
|
||||
data: { displayName, isAdmin, password: hashedPassword, username },
|
||||
data: {
|
||||
displayName,
|
||||
files:
|
||||
image && avatar
|
||||
? {
|
||||
create: {
|
||||
fileName: avatar.fileName,
|
||||
originalName: image?.originalname!,
|
||||
path: avatar.path,
|
||||
size: avatar.size,
|
||||
type: avatar.type,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
isAdmin,
|
||||
password: hashedPassword,
|
||||
username,
|
||||
},
|
||||
include: {
|
||||
files: true,
|
||||
serverFolderPermissions: true,
|
||||
serverPermissions: true,
|
||||
},
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user