mirror of https://github.com/renzynx/bliss.git
added a bunch of stuff
This commit is contained in:
parent
d419d86199
commit
2523dcd57e
|
@ -2,16 +2,16 @@
|
|||
|
||||
- Invite only registration or disable registration completely.
|
||||
- Upload files, images, and videos.
|
||||
- Manage users, roles.
|
||||
- Manage users role.
|
||||
- Limit user quota.
|
||||
- Discord Embed customizer.
|
||||
- Download and delete files.
|
||||
- File preview.
|
||||
- Download and delete files from the dashboard.
|
||||
- Downloadable upload config for ShareX, Flameshot.
|
||||
- A view page for each file.
|
||||
- S3 support (AWS, DigitalOcean, etc.).
|
||||
- Easy installation with docker.
|
||||
- Easy to use admin panel.
|
||||
- Email verification.
|
||||
|
||||
# Installing
|
||||
|
||||
|
@ -63,6 +63,7 @@ docker compose pull && docker compose up -d
|
|||
- `pm2` globally installed
|
||||
- `yarn` globally installed
|
||||
- `caddy` installed
|
||||
- `ffmpeg` installed
|
||||
|
||||
### Backend Installation
|
||||
|
||||
|
@ -125,6 +126,7 @@ Then go through the installation steps again.
|
|||
### Domain name and SSL configuration
|
||||
|
||||
If you don't have caddy installed already
|
||||
<br>
|
||||
[Click Here](https://caddyserver.com/docs/install)
|
||||
|
||||
Copy and paste the following into your terminal:
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
docker-compose.yml
|
||||
docker-compose.yml
|
||||
db_data
|
||||
redis_data
|
||||
|
|
|
@ -12,4 +12,6 @@
|
|||
/.yarn/*
|
||||
!/.yarn/releases
|
||||
!/.yarn/plugins
|
||||
/test
|
||||
/test
|
||||
.env
|
||||
.env.example
|
|
@ -0,0 +1,18 @@
|
|||
FROM node:lts-buster-slim AS development
|
||||
|
||||
ENV NODE_ENV development
|
||||
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json yarn.lock .yarnrc.yml ./
|
||||
COPY .yarn ./.yarn
|
||||
|
||||
RUN yarn install --immutable
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD [ "yarn", "start:dev" ]
|
|
@ -19,5 +19,8 @@ declare namespace NodeJS {
|
|||
S3_REGION: string;
|
||||
CDN_URL: string;
|
||||
UPLOADER: "local" | "s3";
|
||||
// optional env vars
|
||||
COOKIE_NAME?: string;
|
||||
UPLOAD_DIR?: string;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
"cuid": "^2.1.8",
|
||||
"express-session": "^1.17.3",
|
||||
"fast-folder-size": "^1.7.1",
|
||||
"ffmpeg-static": "^5.1.0",
|
||||
"hbs": "^4.2.0",
|
||||
"helmet": "^6.0.0",
|
||||
"ioredis": "^5.2.4",
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "files" ADD COLUMN "cover" TEXT;
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `cover` on the `files` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "files" DROP COLUMN "cover";
|
|
@ -15,6 +15,7 @@ import { RedisService } from "modules/redis/redis.service";
|
|||
import { ThrottlerModule } from "@nestjs/throttler";
|
||||
import { APP_GUARD } from "@nestjs/core";
|
||||
import { ThrottlerBehindProxyGuard } from "modules/root/root.guard";
|
||||
import { S3Service } from "modules/s3/s3.service";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -32,6 +33,7 @@ import { ThrottlerBehindProxyGuard } from "modules/root/root.guard";
|
|||
providers: [
|
||||
RootService,
|
||||
PrismaService,
|
||||
S3Service,
|
||||
RedisService,
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
|
|
|
@ -1,26 +1,67 @@
|
|||
import { uploadDir } from "./constants";
|
||||
import { promises as fs } from "fs";
|
||||
import { CronJob } from "cron";
|
||||
import { join } from "path";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { exec } from "child_process";
|
||||
|
||||
const cleanUp = async () => {
|
||||
// find files start with tmp_ that are older than 24 hours
|
||||
const tmpFiles = (await fs.readdir(uploadDir))
|
||||
.filter((file) => file.startsWith("tmp_"))
|
||||
.filter(async (file) => {
|
||||
const { birthtime } = await fs.stat(join(uploadDir, file));
|
||||
return Date.now() - birthtime.getTime() > 1000 * 60 * 60 * 24;
|
||||
});
|
||||
|
||||
const currentTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
const job = new CronJob(
|
||||
// every 24 hours at 12 AM
|
||||
"0 0 * * *",
|
||||
async () => {
|
||||
console.log("Cleaning up tmp files...");
|
||||
for (const file of tmpFiles) {
|
||||
await fs.unlink(join(uploadDir, file));
|
||||
const startTime = Date.now();
|
||||
|
||||
const logger = new Logger("CronJob");
|
||||
|
||||
const isWindows = process.platform === "win32";
|
||||
|
||||
if (isWindows) {
|
||||
// check if forfiles.exe exists
|
||||
try {
|
||||
await fs.access("C:\\Windows\\System32\\forfiles.exe");
|
||||
} catch (error) {
|
||||
logger.error("forfiles.exe not found, aborting job");
|
||||
logger.log(`Finished job in ${Date.now() - startTime}ms`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { stdout } = await exec(
|
||||
`forfiles /p ${uploadDir} /s /m tmp_* /D -1 /c "cmd /c del @path"`
|
||||
);
|
||||
|
||||
stdout.on("data", (data) => {
|
||||
logger.log(data);
|
||||
});
|
||||
|
||||
logger.log(`Finished job in ${Date.now() - startTime}ms`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const isUnix =
|
||||
process.platform === "linux" ||
|
||||
process.platform === "darwin" ||
|
||||
process.platform === "freebsd" ||
|
||||
process.platform === "openbsd";
|
||||
|
||||
if (isUnix) {
|
||||
const { stdout } = await exec(
|
||||
`find ${uploadDir} -type f -name "tmp_*" -mtime +1 -exec rm -f {} \\;`
|
||||
);
|
||||
|
||||
stdout.on("data", (data) => {
|
||||
logger.log(data);
|
||||
});
|
||||
|
||||
logger.log(`Finished job in ${Date.now() - startTime}ms`);
|
||||
|
||||
return;
|
||||
} else {
|
||||
logger.log("OS not supported");
|
||||
logger.log(`Finished job in ${Date.now() - startTime}ms`);
|
||||
return;
|
||||
}
|
||||
},
|
||||
null,
|
||||
|
@ -28,6 +69,12 @@ const cleanUp = async () => {
|
|||
currentTimeZone
|
||||
);
|
||||
|
||||
const supportedOS = ["win32", "linux", "darwin", "freebsd", "openbsd"];
|
||||
|
||||
if (!supportedOS.includes(process.platform)) {
|
||||
return;
|
||||
}
|
||||
|
||||
job.start();
|
||||
};
|
||||
|
||||
|
|
|
@ -5,14 +5,11 @@ export enum ROUTES {
|
|||
USERS = "users",
|
||||
UPLOAD = "upload",
|
||||
DELETE = "delete",
|
||||
STATISTICS = "statistics",
|
||||
}
|
||||
|
||||
export const rootDir = join(__dirname, "..", "..");
|
||||
export const uploadDir = join(rootDir, "uploads");
|
||||
export const thumbnailDir = join(rootDir, "public");
|
||||
export const uploadDir = process.env.UPLOAD_DIR ?? join(rootDir, "uploads");
|
||||
export const logsDir = join(rootDir, "logs");
|
||||
export const tmpDir = join(rootDir, "tmp");
|
||||
export const COOKIE_NAME = process.env.COOKIE_NAME ?? "auth";
|
||||
export const INVITE_PREFIX = "invite:";
|
||||
export const FORGOT_PASSWORD_PREFIX = "forgot-password:";
|
||||
|
|
|
@ -16,6 +16,7 @@ export interface UserResponse {
|
|||
export interface findUserOptions {
|
||||
byId?: boolean;
|
||||
withPassword?: boolean;
|
||||
withFiles?: boolean;
|
||||
totalUsed?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,9 @@ async function bootstrap() {
|
|||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
secure:
|
||||
process.env.NODE_ENV === "production" &&
|
||||
process.env.USE_SSL === "true",
|
||||
httpOnly: true,
|
||||
maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days
|
||||
sameSite: "lax",
|
||||
|
|
|
@ -8,7 +8,7 @@ import { Request, Response } from "express";
|
|||
import fastFolderSize from "fast-folder-size";
|
||||
import { createReadStream, existsSync } from "fs";
|
||||
import { stat } from "fs/promises";
|
||||
import { thumbnailDir, uploadDir } from "lib/constants";
|
||||
import { uploadDir } from "lib/constants";
|
||||
import { CustomSession } from "lib/types";
|
||||
import {
|
||||
formatBytes,
|
||||
|
@ -17,13 +17,17 @@ import {
|
|||
lookUp,
|
||||
} from "lib/utils";
|
||||
import { PrismaService } from "modules/prisma/prisma.service";
|
||||
import { S3Service } from "modules/s3/s3.service";
|
||||
import { join } from "path";
|
||||
import { promisify } from "util";
|
||||
|
||||
@Injectable()
|
||||
export class RootService {
|
||||
private logger = new Logger(RootService.name);
|
||||
constructor(private readonly prismaService: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly s3Service: S3Service
|
||||
) {}
|
||||
|
||||
downloadFile(filename: string, res: Response) {
|
||||
if (!filename) {
|
||||
|
@ -88,6 +92,7 @@ export class RootService {
|
|||
const isAudio = lookUp(file.filename).includes("audio");
|
||||
const cannotDisplay = !isImage && !isVideo && !isAudio;
|
||||
const timezone = new Date().getTimezoneOffset() / 60;
|
||||
let albumCover = null;
|
||||
|
||||
if (process.env.UPLOADER === "s3") {
|
||||
baseUrl =
|
||||
|
@ -95,21 +100,37 @@ export class RootService {
|
|||
"https://" + process.env.BUCKET_NAME + process.env.S3_ENDPOINT;
|
||||
oembed = `${baseUrl}/${slug}.json`;
|
||||
url = `${baseUrl}/${slug}.${ext}`;
|
||||
await this.s3Service
|
||||
.createInstance()
|
||||
.getObject(process.env.S3_BUCKET_NAME, `${slug}.jpg`, (error) => {
|
||||
if (error) {
|
||||
albumCover = null;
|
||||
} else {
|
||||
albumCover = `${baseUrl}/${slug}.jpg`;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
baseUrl = `${protocol}://${req.headers.host}`;
|
||||
oembed = `${baseUrl}/${slug}.json`;
|
||||
url = `${baseUrl}/${slug}.${ext}`;
|
||||
if (existsSync(join(uploadDir, `${slug}.jpg`))) {
|
||||
albumCover = `${baseUrl}/${slug}.jpg`;
|
||||
} else {
|
||||
albumCover = null;
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
user: { embed_settings },
|
||||
} = file;
|
||||
|
||||
const enabled = embed_settings?.enabled;
|
||||
|
||||
return {
|
||||
oembed,
|
||||
url,
|
||||
title: embed_settings.enabled ? embed_settings?.title : null,
|
||||
description: embed_settings.enabled ? embed_settings?.description : null,
|
||||
title: enabled ? embed_settings?.title : null,
|
||||
description: enabled ? embed_settings?.description : null,
|
||||
color: embed_settings?.color ?? generateRandomHexColor(),
|
||||
ogType: isVideo ? "video.other" : isImage ? "image" : "website",
|
||||
urlType: isVideo ? "video" : isAudio ? "audio" : "image",
|
||||
|
@ -118,13 +139,14 @@ export class RootService {
|
|||
slug: file.slug + "." + file.filename.split(".").pop(),
|
||||
size: formatBytes(file.size),
|
||||
username: file.user.username,
|
||||
embed_enabled: embed_settings?.enabled,
|
||||
embed_enabled: enabled,
|
||||
views: vw,
|
||||
timestamp: formatDate(file.createdAt) + ` (UTC${timezone})`,
|
||||
isVideo,
|
||||
isImage,
|
||||
isAudio,
|
||||
cannotDisplay,
|
||||
albumCover,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -36,6 +36,10 @@ export class S3Service {
|
|||
});
|
||||
}
|
||||
|
||||
createInstance() {
|
||||
return this.s3;
|
||||
}
|
||||
|
||||
createOEmbedJSON(oembed: Partial<EmbedSettings> & { filename: string }) {
|
||||
const { author_name, author_url, provider_name, provider_url, filename } =
|
||||
oembed;
|
||||
|
|
|
@ -14,6 +14,8 @@ import { generateRandomString, lookUp } from "lib/utils";
|
|||
import { PrismaService } from "modules/prisma/prisma.service";
|
||||
import { join } from "path";
|
||||
import md5 from "md5";
|
||||
import { exec } from "node:child_process";
|
||||
import ffmpegPath from "ffmpeg-static";
|
||||
|
||||
@Injectable()
|
||||
export class UploadService {
|
||||
|
@ -206,7 +208,7 @@ export class UploadService {
|
|||
}
|
||||
|
||||
const name = decodeURIComponent(req.headers["x-file-name"] as string);
|
||||
const size = req.headers["x-file-size"] as string;
|
||||
let size = req.headers["x-file-size"] as string;
|
||||
const currentChunk = req.headers["x-current-chunk"] as string;
|
||||
const totalChunks = req.headers["x-total-chunks"] as string;
|
||||
|
||||
|
@ -253,17 +255,37 @@ export class UploadService {
|
|||
|
||||
const { embed_settings } = user;
|
||||
|
||||
if (
|
||||
mimetype.includes("image") &&
|
||||
embed_settings &&
|
||||
embed_settings.enabled
|
||||
) {
|
||||
if (mimetype.includes("image") && embed_settings?.enabled) {
|
||||
this.createOEmbedJSON({
|
||||
filename: name,
|
||||
...embed_settings,
|
||||
});
|
||||
}
|
||||
|
||||
if (mimetype.includes("audio")) {
|
||||
// get album cover if exists
|
||||
const { stderr } = await exec(
|
||||
`${ffmpegPath} -i ${join(
|
||||
uploadDir,
|
||||
`${slug}.${ext}`
|
||||
)} -an -vcodec copy ${join(uploadDir, `${slug}.jpg`)}`
|
||||
);
|
||||
|
||||
if (stderr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
embed_settings?.enabled &&
|
||||
existsSync(join(uploadDir, `${slug}.jpg`))
|
||||
) {
|
||||
this.createOEmbedJSON({
|
||||
filename: name,
|
||||
...embed_settings,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await this.prismaService.file.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
|
|
|
@ -32,7 +32,7 @@ export class UsersController {
|
|||
@SkipThrottle(false)
|
||||
@UseGuards(AuthGuard)
|
||||
@Post("verify/send")
|
||||
async sendVerifyMail(@Request() req: ERequest) {
|
||||
sendVerifyMail(@Request() req: ERequest) {
|
||||
return this.usersService.sendVerifyEmail(
|
||||
(req.session as CustomSession).userId
|
||||
);
|
||||
|
@ -40,32 +40,32 @@ export class UsersController {
|
|||
|
||||
@UseGuards(AuthGuard)
|
||||
@Post("verify")
|
||||
async verifyEmail(@Body() { token }: { token: string }) {
|
||||
verifyEmail(@Body() { token }: { token: string }) {
|
||||
return this.usersService.verifyEmail(token);
|
||||
}
|
||||
|
||||
@SkipThrottle(false)
|
||||
@Post("forgot-password")
|
||||
async forgotPassword(@Body() { email }: { email: string }) {
|
||||
forgotPassword(@Body() { email }: { email: string }) {
|
||||
return this.usersService.sendForgotPasswordEmail(email);
|
||||
}
|
||||
|
||||
@SkipThrottle(false)
|
||||
@Post("check-token")
|
||||
async checkToken(@Body() { token }: { token: string }) {
|
||||
checkToken(@Body() { token }: { token: string }) {
|
||||
return this.usersService.checkToken(token);
|
||||
}
|
||||
|
||||
@UseFilters(new HttpExceptionFilter())
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
@Post("reset-password")
|
||||
async resetPassword(@Body() { token, password }: ResetPasswordDTO) {
|
||||
resetPassword(@Body() { token, password }: ResetPasswordDTO) {
|
||||
return this.usersService.resetPassword(token, password);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Get("files")
|
||||
async getFiles(
|
||||
getFiles(
|
||||
@Request() req: ERequest,
|
||||
@Query("skip") skip: string,
|
||||
@Query("take") take: string,
|
||||
|
@ -90,10 +90,7 @@ export class UsersController {
|
|||
@UseFilters(new HttpExceptionFilter())
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
@Put("embed-settings")
|
||||
async updateEmbedSettings(
|
||||
@Request() req: ERequest,
|
||||
@Body() body: EmbedSettingDTO
|
||||
) {
|
||||
updateEmbedSettings(@Request() req: ERequest, @Body() body: EmbedSettingDTO) {
|
||||
return this.usersService.setEmbedSettings(
|
||||
body,
|
||||
(req.session as CustomSession).userId
|
||||
|
@ -102,7 +99,7 @@ export class UsersController {
|
|||
|
||||
@UseGuards(AuthGuard)
|
||||
@Get("embed-settings")
|
||||
async getEmbedSettings(@Request() req: ERequest) {
|
||||
getEmbedSettings(@Request() req: ERequest) {
|
||||
return this.usersService.getEmbedSettings(
|
||||
(req.session as CustomSession).userId
|
||||
);
|
||||
|
@ -113,7 +110,7 @@ export class UsersController {
|
|||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
@UseFilters(new HttpExceptionFilter())
|
||||
@Put("change-password")
|
||||
async changePassword(
|
||||
changePassword(
|
||||
@Request() req: ERequest,
|
||||
@Body()
|
||||
{ password, newPassword }: ChangePasswordDTO
|
||||
|
@ -130,7 +127,7 @@ export class UsersController {
|
|||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
@UseFilters(new HttpExceptionFilter())
|
||||
@Put("change-username")
|
||||
async changeUsername(
|
||||
changeUsername(
|
||||
@Body()
|
||||
{ username, newUsername }: ChangeUsernameDTO
|
||||
) {
|
||||
|
@ -141,7 +138,7 @@ export class UsersController {
|
|||
@Throttle(1, 300)
|
||||
@UseGuards(AuthGuard)
|
||||
@Put("regenerate-api-key")
|
||||
async regnerateApiKey(@Request() req: ERequest) {
|
||||
regnerateApiKey(@Request() req: ERequest) {
|
||||
return this.usersService.regenerateApiKey(
|
||||
(req.session as CustomSession).userId
|
||||
);
|
||||
|
@ -149,9 +146,15 @@ export class UsersController {
|
|||
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete("delete-account")
|
||||
async deleteAccount(@Request() req: ERequest) {
|
||||
deleteAccount(@Request() req: ERequest) {
|
||||
return this.usersService.deleteAccount(
|
||||
(req.session as CustomSession).userId
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete("wipe-files")
|
||||
wipeFiles(@Request() req: ERequest) {
|
||||
return this.usersService.wipeFiles((req.session as CustomSession).userId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
Logger,
|
||||
UnauthorizedException,
|
||||
} from "@nestjs/common";
|
||||
import { EmbedSettings, User } from "@prisma/client";
|
||||
import { EmbedSettings, File, User } from "@prisma/client";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||
import * as argon from "argon2";
|
||||
import { MailService } from "../mail/mail.service";
|
||||
|
@ -29,9 +29,10 @@ import {
|
|||
FORGOT_PASSWORD_PREFIX,
|
||||
INVITE_PREFIX,
|
||||
rootDir,
|
||||
uploadDir,
|
||||
} from "lib/constants";
|
||||
import { join } from "path";
|
||||
import { readFile } from "fs/promises";
|
||||
import { readFile, unlink } from "fs/promises";
|
||||
import { EmbedSettingDTO } from "./dto/EmbedSettingsDTO";
|
||||
import cuid from "cuid";
|
||||
|
||||
|
@ -49,12 +50,13 @@ export class UsersService implements IUserService {
|
|||
|
||||
async findUser(
|
||||
username_or_email: string,
|
||||
{ byId, withPassword, totalUsed }: findUserOptions = {
|
||||
{ byId, withPassword, totalUsed, withFiles }: findUserOptions = {
|
||||
byId: false,
|
||||
withPassword: false,
|
||||
totalUsed: false,
|
||||
withFiles: false,
|
||||
}
|
||||
): Promise<User | null> {
|
||||
): Promise<(User & { files?: File[] }) | null> {
|
||||
if (!username_or_email) {
|
||||
throw new BadRequestException("Invalid request");
|
||||
}
|
||||
|
@ -68,6 +70,9 @@ export class UsersService implements IUserService {
|
|||
where: {
|
||||
id: username_or_email,
|
||||
},
|
||||
include: {
|
||||
files: withFiles,
|
||||
},
|
||||
});
|
||||
if (totalUsed) {
|
||||
const tmp = await this.prisma.file.aggregate({
|
||||
|
@ -87,6 +92,7 @@ export class UsersService implements IUserService {
|
|||
where: username_or_email.includes("@")
|
||||
? { email: username_or_email }
|
||||
: { username: username_or_email },
|
||||
include: { files: withFiles },
|
||||
});
|
||||
|
||||
if (totalUsed) {
|
||||
|
@ -558,8 +564,31 @@ export class UsersService implements IUserService {
|
|||
return true;
|
||||
}
|
||||
|
||||
async wipeFiles(id: string) {
|
||||
const files = await this.prisma.file.findMany({ where: { userId: id } });
|
||||
|
||||
const promises = files.map((file) => {
|
||||
const ext = file.filename.split(".").pop();
|
||||
const filename = `${file.slug}.${ext}`;
|
||||
if (file.mimetype.includes("audio")) {
|
||||
return Promise.all([
|
||||
unlink(join(uploadDir, filename)),
|
||||
unlink(join(uploadDir, `${file.slug}.jpg`)),
|
||||
]);
|
||||
}
|
||||
return unlink(join(uploadDir, filename));
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
this.prisma.file.deleteMany({ where: { userId: id } }),
|
||||
...promises,
|
||||
]).catch(() => {});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async deleteAccount(id: string) {
|
||||
const user = await this.findUser(id, { byId: true });
|
||||
const user = await this.findUser(id, { byId: true, withFiles: true });
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException("not authorized");
|
||||
|
@ -573,9 +602,11 @@ export class UsersService implements IUserService {
|
|||
throw new BadRequestException("You cannot delete root account");
|
||||
}
|
||||
|
||||
await this.prisma.user.delete({ where: { id } });
|
||||
const wiped = await this.wipeFiles(id);
|
||||
|
||||
return true;
|
||||
await this.prisma.user.delete({ where: { id } }).catch(() => {});
|
||||
|
||||
return wiped;
|
||||
}
|
||||
|
||||
async sendForgotPasswordEmail(email: string) {
|
||||
|
@ -590,7 +621,12 @@ export class UsersService implements IUserService {
|
|||
});
|
||||
}
|
||||
|
||||
const isEmail = /\S+@\S+\.\S+/.test(email);
|
||||
// RFC 5322 Official Standard
|
||||
const emailRegex = new RegExp(
|
||||
/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/g
|
||||
);
|
||||
|
||||
const isEmail = emailRegex.test(email);
|
||||
|
||||
if (!isEmail) {
|
||||
throw new BadRequestException({
|
||||
|
|
|
@ -86,6 +86,14 @@
|
|||
</video>
|
||||
{{/if}}
|
||||
{{#if isAudio}}
|
||||
{{#if albumCover}}
|
||||
<img
|
||||
decoding="async"
|
||||
src="{{albumCover}}"
|
||||
class="card-img-top mb-3"
|
||||
alt="Album Cover"
|
||||
/>
|
||||
{{/if}}
|
||||
<audio controls loop preload="metadata">
|
||||
<source src="{{url}}" type="{{mimetype}}" />
|
||||
Your browser does't support this audio type
|
||||
|
|
|
@ -491,6 +491,18 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@derhuerst/http-basic@npm:^8.2.0":
|
||||
version: 8.2.4
|
||||
resolution: "@derhuerst/http-basic@npm:8.2.4"
|
||||
dependencies:
|
||||
caseless: ^0.12.0
|
||||
concat-stream: ^2.0.0
|
||||
http-response-object: ^3.0.1
|
||||
parse-cache-control: ^1.0.1
|
||||
checksum: dfb2f30c23fb907988d1c34318fa74c54dcd3c3ba6b4b0e64cdb584d03303ad212dd3b3874328a9367d7282a232976acbd33a20bb9c7a6ea20752e879459253b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/eslintrc@npm:^1.3.3":
|
||||
version: 1.3.3
|
||||
resolution: "@eslint/eslintrc@npm:1.3.3"
|
||||
|
@ -1478,6 +1490,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^10.0.3":
|
||||
version: 10.17.60
|
||||
resolution: "@types/node@npm:10.17.60"
|
||||
checksum: 2cdb3a77d071ba8513e5e8306fa64bf50e3c3302390feeaeff1fd325dd25c8441369715dfc8e3701011a72fed5958c7dfa94eb9239a81b3c286caa4d97db6eef
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^16.0.0":
|
||||
version: 16.18.3
|
||||
resolution: "@types/node@npm:16.18.3"
|
||||
|
@ -2120,6 +2139,7 @@ __metadata:
|
|||
eslint-plugin-prettier: ^4.0.0
|
||||
express-session: ^1.17.3
|
||||
fast-folder-size: ^1.7.1
|
||||
ffmpeg-static: ^5.1.0
|
||||
hbs: ^4.2.0
|
||||
helmet: ^6.0.0
|
||||
ioredis: ^5.2.4
|
||||
|
@ -2721,6 +2741,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"caseless@npm:^0.12.0":
|
||||
version: 0.12.0
|
||||
resolution: "caseless@npm:0.12.0"
|
||||
checksum: b43bd4c440aa1e8ee6baefee8063b4850fd0d7b378f6aabc796c9ec8cb26d27fb30b46885350777d9bd079c5256c0e1329ad0dc7c2817e0bb466810ebb353751
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chainsaw@npm:~0.1.0":
|
||||
version: 0.1.0
|
||||
resolution: "chainsaw@npm:0.1.0"
|
||||
|
@ -3035,6 +3062,18 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"concat-stream@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "concat-stream@npm:2.0.0"
|
||||
dependencies:
|
||||
buffer-from: ^1.0.0
|
||||
inherits: ^2.0.3
|
||||
readable-stream: ^3.0.2
|
||||
typedarray: ^0.0.6
|
||||
checksum: d7f75d48f0ecd356c1545d87e22f57b488172811b1181d96021c7c4b14ab8855f5313280263dca44bb06e5222f274d047da3e290a38841ef87b59719bde967c7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"connect-redis@npm:^6.1.3":
|
||||
version: 6.1.3
|
||||
resolution: "connect-redis@npm:6.1.3"
|
||||
|
@ -4011,6 +4050,18 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ffmpeg-static@npm:^5.1.0":
|
||||
version: 5.1.0
|
||||
resolution: "ffmpeg-static@npm:5.1.0"
|
||||
dependencies:
|
||||
"@derhuerst/http-basic": ^8.2.0
|
||||
env-paths: ^2.2.0
|
||||
https-proxy-agent: ^5.0.0
|
||||
progress: ^2.0.3
|
||||
checksum: 0e27d671a0be1f585ef03e48c2af7c2be14f4e61470ffa02e3b8919551243ee854028a898dfcd16cdf1e3c01916f3c5e9938f42cbc7e877d7dd80d566867db8b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"figures@npm:^3.0.0":
|
||||
version: 3.2.0
|
||||
resolution: "figures@npm:3.2.0"
|
||||
|
@ -4593,6 +4644,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"http-response-object@npm:^3.0.1":
|
||||
version: 3.0.2
|
||||
resolution: "http-response-object@npm:3.0.2"
|
||||
dependencies:
|
||||
"@types/node": ^10.0.3
|
||||
checksum: 6cbdcb4ce7b27c9158a131b772c903ed54add2ba831e29cc165e91c3969fa6f8105ddf924aac5b954b534ad15a1ae697b693331b2be5281ee24d79aae20c3264
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"https-proxy-agent@npm:^5.0.0":
|
||||
version: 5.0.1
|
||||
resolution: "https-proxy-agent@npm:5.0.1"
|
||||
|
@ -6473,6 +6533,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parse-cache-control@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "parse-cache-control@npm:1.0.1"
|
||||
checksum: 5a70868792124eb07c2dd07a78fcb824102e972e908254e9e59ce59a4796c51705ff28196d2b20d3b7353d14e9f98e65ed0e4eda9be072cc99b5297dc0466fee
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parse-json@npm:^5.0.0, parse-json@npm:^5.2.0":
|
||||
version: 5.2.0
|
||||
resolution: "parse-json@npm:5.2.0"
|
||||
|
@ -6647,6 +6714,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"progress@npm:^2.0.3":
|
||||
version: 2.0.3
|
||||
resolution: "progress@npm:2.0.3"
|
||||
checksum: f67403fe7b34912148d9252cb7481266a354bd99ce82c835f79070643bb3c6583d10dbcfda4d41e04bbc1d8437e9af0fb1e1f2135727878f5308682a579429b7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"promise-inflight@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "promise-inflight@npm:1.0.1"
|
||||
|
@ -6824,7 +6898,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:2 || 3, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0":
|
||||
"readable-stream@npm:2 || 3, readable-stream@npm:^3.0.2, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0":
|
||||
version: 3.6.0
|
||||
resolution: "readable-stream@npm:3.6.0"
|
||||
dependencies:
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
version: '3.9'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:latest
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: postgres
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
restart: always
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
api:
|
||||
build:
|
||||
context: ./api
|
||||
dockerfile: Dockerfile.dev
|
||||
restart: always
|
||||
volumes:
|
||||
- ./api:/app
|
||||
ports:
|
||||
- 3000:3000
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres?schema=public&connect_timeout=300
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- CORS_ORIGIN=http://localhost:5000
|
||||
|
||||
- SESSION_SECRET=
|
||||
- USE_MAIL=true
|
||||
- MAIL_HOST=
|
||||
- MAIL_PORT=
|
||||
- MAIL_USER=
|
||||
- MAIL_PASS=
|
||||
- MAIL_FROM=Bliss <noreply@amog-us.club>
|
||||
# s3 or local
|
||||
- UPLOADER=local
|
||||
# if you use s3 set these
|
||||
- S3_ENDPOINT=
|
||||
- S3_ACCESS_KEY_ID=
|
||||
- S3_SECRET_ACCESS_KEY=
|
||||
- S3_BUCKET_NAME=
|
||||
- S3_REGION=
|
||||
|
||||
# optional
|
||||
- COOKIE_NAME=
|
||||
- UPLOAD_DIR=
|
||||
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
web:
|
||||
build:
|
||||
context: ./web
|
||||
dockerfile: Dockerfile.dev
|
||||
restart: always
|
||||
volumes:
|
||||
- ./web:/app
|
||||
ports:
|
||||
- 5000:5000
|
||||
depends_on:
|
||||
- api
|
||||
environment:
|
||||
- NEXT_PUBLIC_API_URL=http://localhost:3000
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
redis_data:
|
|
@ -43,14 +43,24 @@ services:
|
|||
- MAIL_PASS=
|
||||
# e.g. ServiceName <noreply@domain.com>
|
||||
- MAIL_FROM=
|
||||
# should be local or s3, s3 doesn't work yet
|
||||
- UPLOADER=local
|
||||
# set to "true" if you are going to use a reverse proxy
|
||||
- USE_PROXY=false
|
||||
# set to "true" if you are going to use SSL
|
||||
- USE_SSL=false
|
||||
# port for the container to listen on
|
||||
- PORT=3000
|
||||
|
||||
# should be local or s3
|
||||
- UPLOADER=local
|
||||
# s3 config (only needed if you are using s3)
|
||||
- S3_ENDPOINT=
|
||||
- S3_ACCESS_KEY_ID=
|
||||
- S3_SECRET_ACCESS_KEY=
|
||||
- S3_BUCKET_NAME=
|
||||
- S3_REGION=
|
||||
|
||||
# optional
|
||||
- COOKIE_NAME=
|
||||
ports:
|
||||
# 👇 Change this to whatever port you want
|
||||
- 8080:3000
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
FROM node:lts-buster-slim AS development
|
||||
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV NODE_ENV development
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json yarn.lock .yarnrc.yml ./
|
||||
COPY .yarn ./.yarn
|
||||
|
||||
RUN yarn install --immutable
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD [ "yarn", "dev" ]
|
|
@ -1,5 +1,5 @@
|
|||
import { API_URL } from '@lib/constants';
|
||||
import { IFile } from '@lib/types';
|
||||
import { FileResponse, IFile } from '@lib/types';
|
||||
import { formatDate } from '@lib/utils';
|
||||
import {
|
||||
Box,
|
||||
|
@ -7,26 +7,20 @@ import {
|
|||
Flex,
|
||||
Grid,
|
||||
Group,
|
||||
// Paper,
|
||||
Image,
|
||||
Stack,
|
||||
Text,
|
||||
} from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconCheck, IconX } from '@tabler/icons';
|
||||
import {
|
||||
QueryObserverResult,
|
||||
RefetchOptions,
|
||||
RefetchQueryFilters,
|
||||
} from '@tanstack/react-query';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { FC, useCallback } from 'react';
|
||||
|
||||
const PreviewCard: FC<{
|
||||
file: IFile;
|
||||
refetch: <TPageData>(
|
||||
options?: (RefetchOptions & RefetchQueryFilters<TPageData>) | undefined
|
||||
) => Promise<QueryObserverResult<any, unknown>>;
|
||||
}> = ({ file, refetch }) => {
|
||||
}> = ({ file }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const fileURL = `${API_URL}/${file.slug}.${file.filename.split('.').pop()}`;
|
||||
const deleteFile = useCallback(() => {
|
||||
return axios
|
||||
|
@ -39,9 +33,25 @@ const PreviewCard: FC<{
|
|||
message: data,
|
||||
icon: <IconCheck />,
|
||||
});
|
||||
refetch({ queryKey: ['files'], exact: true });
|
||||
queryClient.setQueryData<FileResponse>(['files'], (oldData) => {
|
||||
if (!oldData) return { files: [], totalFiles: 0, totalPages: 0 };
|
||||
return {
|
||||
files: oldData?.files.filter((f: IFile) => f.id !== file.id),
|
||||
totalFiles: oldData?.totalFiles - 1,
|
||||
totalPages: oldData?.totalPages,
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
if (err.response.status === 429) {
|
||||
showNotification({
|
||||
title: 'Delete File',
|
||||
color: 'red',
|
||||
message: 'You are deleting files too fast.',
|
||||
icon: <IconX />,
|
||||
});
|
||||
return;
|
||||
}
|
||||
showNotification({
|
||||
title: 'Delete File',
|
||||
color: 'red',
|
||||
|
@ -49,7 +59,7 @@ const PreviewCard: FC<{
|
|||
icon: <IconX />,
|
||||
});
|
||||
});
|
||||
}, [file.id, refetch]);
|
||||
}, [file.id, queryClient]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
|
@ -62,7 +72,6 @@ const PreviewCard: FC<{
|
|||
border: `1px solid ${theme.colors.dark[4]}`,
|
||||
borderRadius: theme.radius.md,
|
||||
boxShadow: theme.shadows.sm,
|
||||
maxHeight: 500,
|
||||
msOverflowStyle: 'none',
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: `${theme.colors.dark[5]} ${theme.colors.dark[7]}`,
|
||||
|
@ -113,9 +122,17 @@ const PreviewCard: FC<{
|
|||
<source src={fileURL} />
|
||||
</video>
|
||||
) : file.mimetype.includes('audio') ? (
|
||||
<audio controls loop preload="metadata">
|
||||
<source src={fileURL} />
|
||||
</audio>
|
||||
<Stack align="center">
|
||||
<Image
|
||||
width={300}
|
||||
height={300}
|
||||
src={`${API_URL}/${file.slug}.jpg`}
|
||||
alt="Album Cover"
|
||||
/>
|
||||
<audio controls loop preload="metadata">
|
||||
<source src={fileURL} />
|
||||
</audio>
|
||||
</Stack>
|
||||
) : (
|
||||
<Text align="center">
|
||||
This file type is not supported for preview.
|
||||
|
|
|
@ -15,15 +15,15 @@ import {
|
|||
} from '@mantine/core';
|
||||
import { useDebouncedState } from '@mantine/hooks';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Suspense, useEffect, useMemo, useState } from 'react';
|
||||
const PreviewCard = dynamic(() => import('./PreviewCard'));
|
||||
import { Suspense, useMemo, useState, memo } from 'react';
|
||||
const PreviewCard = memo(dynamic(() => import('./PreviewCard')));
|
||||
|
||||
const FileViewer = () => {
|
||||
const [value, setValue] = useDebouncedState('', 200);
|
||||
const [page, setPage] = useState(1);
|
||||
const [limit, setLimit] = useState<number | 'all'>(15);
|
||||
const [sort, setSort] = useState<'newest' | 'oldest'>('newest');
|
||||
const { data, isFetching, error, refetch, isLoading } = useGetUserFiles({
|
||||
const { data, isFetching, error, isLoading } = useGetUserFiles({
|
||||
currentPage: page,
|
||||
skip: limit !== 'all' ? limit * (page - 1) : 0,
|
||||
take: limit !== 'all' ? limit : 'all',
|
||||
|
@ -31,19 +31,15 @@ const FileViewer = () => {
|
|||
search: value,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refetch({ queryKey: ['files'], exact: true });
|
||||
}, [page, limit, sort, value, refetch]);
|
||||
|
||||
const files = useMemo(() => {
|
||||
if (data?.files) {
|
||||
return data.files.map((file: IFile) => (
|
||||
<PreviewCard refetch={refetch} key={file.id} file={file} />
|
||||
<PreviewCard key={file.id} file={file} />
|
||||
));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [data?.files, refetch]);
|
||||
}, [data?.files]);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingPage color="yellow" />;
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import { Button, Group, Text, TextInput } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { API_ROUTES, API_URL } from '@lib/constants';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconCheck, IconExclamationMark } from '@tabler/icons';
|
||||
const Modal = dynamic(() => import('@mantine/core').then((mod) => mod.Modal));
|
||||
|
||||
const WipeFiles = () => {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [confirm, setConfirm] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const { mutateAsync, isLoading } = useMutation(['wipe-files'], () =>
|
||||
axios
|
||||
.delete(API_URL + API_ROUTES.WIPE_FILES, { withCredentials: true })
|
||||
.catch((err) => {
|
||||
throw new Error(err.response.data.message);
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
centered
|
||||
withCloseButton={false}
|
||||
>
|
||||
<Text weight="bold" size="lg" color="red" my="xs">
|
||||
Danger Zone
|
||||
</Text>
|
||||
|
||||
<TextInput
|
||||
label="Type 'delete' to confirm"
|
||||
description="This action cannot be undone"
|
||||
error={error}
|
||||
value={confirm}
|
||||
placeholder="delete"
|
||||
onChange={(e) => {
|
||||
setError('');
|
||||
setConfirm(e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<Group position="right" mt="md">
|
||||
<Button variant="default" onClick={() => setOpened(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
loading={isLoading}
|
||||
onClick={() => {
|
||||
if (confirm === 'delete') {
|
||||
mutateAsync()
|
||||
.then(() => {
|
||||
showNotification({
|
||||
title: 'Success',
|
||||
message: 'Your files has been deleted sucessfully.',
|
||||
color: 'green',
|
||||
icon: <IconCheck />,
|
||||
autoClose: 5000,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(err.message);
|
||||
showNotification({
|
||||
title: 'Error',
|
||||
message: err.message,
|
||||
color: 'red',
|
||||
icon: <IconExclamationMark />,
|
||||
autoClose: 5000,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
setError('Please type "delete" to confirm');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Wipe Files
|
||||
</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
<Button onClick={() => setOpened(true)} color="red">
|
||||
Wipe Files
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WipeFiles;
|
|
@ -21,6 +21,7 @@ import { useSendVerificationEmail, useSignOut } from '@lib/hooks';
|
|||
import { showNotification } from '@mantine/notifications';
|
||||
import { profileStyles } from './styles';
|
||||
import DeleteAccount from './DeleteAccount';
|
||||
import WipeFiles from './WipeFiles';
|
||||
const Modal = dynamic(() => import('@mantine/core').then((mod) => mod.Modal));
|
||||
const ChangePasswordForm = dynamic(() => import('./ChangePasswordForm'));
|
||||
|
||||
|
@ -150,7 +151,11 @@ const ProfilePage = () => {
|
|||
<Divider mt="xl" mb="xl" />
|
||||
<CredentialsForm username={user?.username!} />
|
||||
<Divider mt="xl" mb="xl" />
|
||||
<DeleteAccount />
|
||||
<Group spacing="xl">
|
||||
<DeleteAccount />
|
||||
<Divider orientation="vertical" />
|
||||
<WipeFiles />
|
||||
</Group>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
|
|
|
@ -31,6 +31,7 @@ export enum API_ROUTES {
|
|||
FORGOT_PASSWORD = '/users/forgot-password',
|
||||
RESET_PASSWORD = '/users/reset-password',
|
||||
DELETE_ACCOUNT = '/users/delete-account',
|
||||
WIPE_FILES = '/users/wipe-files',
|
||||
CHECK_TOKEN = '/users/check-token',
|
||||
SEND_VERIFICATION_EMAIL = '/users/verify/send',
|
||||
VERIFY_EMAIL = '/users/verify',
|
||||
|
|
|
@ -16,8 +16,8 @@ export const useGetUserFiles = ({
|
|||
sort?: string;
|
||||
search?: string;
|
||||
}) => {
|
||||
const { data, isLoading, error, isFetching, refetch } = useQuery(
|
||||
['files'],
|
||||
const { data, isLoading, error, isFetching } = useQuery(
|
||||
['files', skip, take, sort, search, currentPage],
|
||||
() =>
|
||||
axios
|
||||
.get<FileResponse>(
|
||||
|
@ -28,7 +28,9 @@ export const useGetUserFiles = ({
|
|||
withCredentials: true,
|
||||
}
|
||||
)
|
||||
.then((res) => res.data)
|
||||
.then((res) => {
|
||||
return res.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw new Error(error.response.data.message);
|
||||
}),
|
||||
|
@ -39,5 +41,5 @@ export const useGetUserFiles = ({
|
|||
}
|
||||
);
|
||||
|
||||
return { data, isLoading, isFetching, error, refetch };
|
||||
return { data, isLoading, isFetching, error };
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue