mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
Compare commits
16 Commits
v2.0.0-bet
...
v2.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
c1baa3a16d | ||
|
cfc103e056 | ||
|
1a0b565ae0 | ||
|
37a30f1bd7 | ||
|
d7bdffe096 | ||
|
277fa3ce28 | ||
|
11b2c5d9a1 | ||
|
0e1ea0f2ef | ||
|
a8c53afe8c | ||
|
5b3dab7c75 | ||
|
57d89e5807 | ||
|
107a467bcc | ||
|
792183bbb3 | ||
|
a12183d4a8 | ||
|
359d0a0403 | ||
|
c3967eca72 |
@@ -1,108 +0,0 @@
|
||||
---
|
||||
title: 👋 First Login (Admin)
|
||||
---
|
||||
After successfully initiating all required services according to the detailed deployment instructions provided, you will gain access to the frontend interface through the following designated endpoints:
|
||||
|
||||
- **Production environment:** `{your_web_domain}` or `{your_server_ip}` - This is your primary access point for the live application deployment
|
||||
- **Local environment:** [http://localhost:4173](http://localhost:4173/) - This endpoint is specifically for development and testing purposes
|
||||
|
||||
Upon successfully navigating to the frontend interface through either of these endpoints, you will be presented with a comprehensive welcome screen, as illustrated in the image below:
|
||||
|
||||

|
||||
|
||||
What you are viewing is the **Palmr. landing page** ✨, which serves as an introductory interface providing essential information about the application's capabilities and features. While this landing page is displayed by default during your initial setup, you have the flexibility to modify this configuration later. Should you prefer, you can configure the system to bypass this landing page and present the login interface as your primary entry point. However, for the purposes of initial system familiarization, the landing page is deliberately set as the default welcome screen.
|
||||
|
||||
---
|
||||
|
||||
## 🔑 First Login
|
||||
|
||||
Prominently displayed at the top of the landing page, you will notice a clearly marked button that enables you to authenticate and access Palmr.:
|
||||
|
||||
> But you may wonder:
|
||||
>
|
||||
|
||||
To facilitate a smooth onboarding experience, Palmr. has been thoughtfully designed with pre-configured **seed data** that is automatically implemented during the initial system initialization. This preliminary data setup includes a comprehensive **admin user** account that is granted complete access privileges, enabling full control over all system settings and user management functionalities within the application environment.
|
||||
|
||||
Upon selecting the **Login** button, the system will seamlessly redirect you to the authentication interface, which presents itself as shown in the following image:
|
||||
|
||||

|
||||
|
||||
For your initial access to the system, please utilize these pre-configured authentication credentials:
|
||||
|
||||
### 👤 Admin Credentials
|
||||
|
||||
| User | Password |
|
||||
| --- | --- |
|
||||
| `admin@example.com` | `admin123` |
|
||||
|
||||
Following successful validation of your credentials, the system will authenticate your session and automatically direct you to Palmr.'s primary dashboard interface, which appears as demonstrated in this image:
|
||||
|
||||

|
||||
|
||||
Having completed these steps successfully, you have now established an authenticated session and are fully prepared to explore and utilize the comprehensive feature set that Palmr. has to offer! 🎉
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Recommendations After First Access
|
||||
|
||||
Given that Palmr.'s initial configuration includes a solitary default admin user within the seed data, it is **strongly recommended** as a security best practice to modify the default administrative credentials immediately following your first successful login. This crucial step is fundamental to establishing and maintaining the security integrity of your Palmr. instance.
|
||||
|
||||
Please follow this detailed sequence of steps to update your administrative credentials and enhance the security of your Palmr. installation:
|
||||
|
||||
### 🔧 Access the Profile Settings
|
||||
|
||||
1. Locate and click the **user icon** positioned in the upper right corner of your screen interface.
|
||||
2. Upon clicking, you will be presented with an expandable dropdown menu containing several configuration options:
|
||||
|
||||

|
||||
|
||||
1. From the available options in the dropdown menu, select **"Profile"**. This selection will navigate you to the comprehensive profile settings interface:
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 📝 Update the Admin Profile
|
||||
|
||||
Within the profile settings interface, you have full access to modify all information associated with the administrative account.
|
||||
|
||||
- To enhance security through password modification, input and confirm your newly chosen secure password.
|
||||
- Take this opportunity to review and adjust any additional profile details according to your specific requirements and preferences.
|
||||
|
||||
**Tip:** ✨ To establish robust security measures, ensure your new password incorporates the following elements:
|
||||
|
||||
- A minimum length of 12 characters to provide adequate complexity
|
||||
- A diverse combination of uppercase and lowercase alphabetical characters
|
||||
- An assortment of numerical digits and special character symbols
|
||||
|
||||
---
|
||||
|
||||
### 📸 Update the Profile Picture
|
||||
|
||||
To enhance the personalization of your administrative profile, you have the option to customize your profile picture.
|
||||
|
||||
1. Identify and select the **camera icon** positioned adjacent to your current avatar display.
|
||||
2. Browse and select an appropriate image file from your local storage device.
|
||||
|
||||

|
||||
|
||||
> 💡 Recommendation: For optimal visual presentation, utilize an image with equal width and height dimensions (square format).
|
||||
>
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Troubleshooting
|
||||
|
||||
Should you encounter any technical difficulties during the initial authentication process or while updating your profile information, please verify the following system components:
|
||||
|
||||
- Confirm the operational status of all essential services, including the frontend interface, backend systems, MinIO storage, and database connections.
|
||||
- Verify the accurate configuration of all necessary environment variables within your system.
|
||||
- Ensure the successful and complete application of all database seed operations.
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Best Practices
|
||||
|
||||
- Following the successful configuration of your administrative account, establish additional user accounts with appropriately restricted access permissions based on specific roles and responsibilities.
|
||||
- Implement HTTPS protocol to ensure secure data transmission between client devices and the server infrastructure.
|
||||
- Maintain system security by regularly implementing updates to your Palmr. installation to incorporate the latest security patches and system improvements.
|
@@ -49,6 +49,7 @@ services:
|
||||
- MINIO_BUCKET_NAME=files # MinIO bucket name - This is needed for MinIO to work properly, dont change it if you don't know what you are doing
|
||||
- FRONTEND_URL=${APP_URL:-http://${SERVER_IP:-localhost}:${APP_EXTERNAL_PORT:-5487}} # Frontend URL - Make sure to use the correct frontend URL, depends on where the frontend is running, its prepared for localhost, but you can change it to your frontend URL if needed
|
||||
- SERVER_IP=${SERVER_IP:-localhost} # Server IP - Make sure to use the correct server IP if you running on a cloud provider or a virtual machine. This prepared for localhost, but you can change it to your server IP if needed
|
||||
- MAX_FILESIZE=${MAX_FILESIZE:-1073741824} # Max Filesize for upload - Declared in Bytes. Default is 1GiB
|
||||
ports:
|
||||
- "${API_EXTERNAL_PORT:-3333}:${API_INTERNAL_PORT:-3333}" # Backend port mapping
|
||||
restart: unless-stopped
|
||||
@@ -179,6 +180,7 @@ The table below shows all environment variables that can be set
|
||||
| MINIO_EXTERNAL_CONSOLE_PORT | 6422 | Exposed port on host for MinIO console |
|
||||
| POSTGRESQL_USERNAME | postgres | PostgreSQL user |
|
||||
| POSTGRESQL_DATABASE | palmr_db | Database name |
|
||||
| MAX_FILESIZE | 1073741824 | Max Uploadsize per file. Unit in Bytes |
|
||||
|
||||
> *All these variables can be configured through a .env file in the project root or defined directly in the environment where docker-compose will be executed. The best way to do this is up to you. But be careful to replace correctly if doing directly in the compose instead of providing an environment var.*
|
||||
>
|
||||
@@ -219,12 +221,21 @@ We mainly need to pay attention to the following points:
|
||||
- For all environment variables that are `PASSWORD`, it's highly recommended to generate secure passwords and replace them as env vars.
|
||||
- Lastly, make sure no docker service will conflict with any existing ones in your environment. If there is a conflict, simply change the execution ports via environment var or in the docker compose.
|
||||
|
||||
To generate a .env file with just the `server_ip` configuration, you can run this command:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://gist.githubusercontent.com/danielalves96/5a68913d70e5e31b68b7331dc17dfa9c/raw | bash
|
||||
```
|
||||
> execute this command in your server terminal, at same path of docker-compose.yaml.
|
||||
|
||||
Basically, by paying attention to these points, you can quickly execute the project with the same command we used for localhost:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
⚠️ This makes sure you're always running the latest beta version of the image, otherwise, Docker might reuse an outdated one from cache.
|
||||
|
||||
At this stage, if you encounter any errors, it's worth reviewing your `docker-compose.yaml` and trying again, paying close attention to the points mentioned above.
|
||||
|
||||
> *First test without using reverse proxies like Caddy, Traefik, etc... if you plan to use them. Access the services via `server_ip:port` after confirming they work, then make the necessary routing configurations as desired.*
|
||||
|
@@ -12,7 +12,6 @@
|
||||
"manual-installation",
|
||||
"api",
|
||||
"---How to use Palmr.---",
|
||||
"first-login",
|
||||
"manage-users",
|
||||
"uploading-files",
|
||||
"generate-share",
|
||||
|
@@ -2,9 +2,6 @@ import { source } from '@/lib/source';
|
||||
import { createFromSource } from 'fumadocs-core/search/server';
|
||||
|
||||
export const { GET } = createFromSource(source, (page) => {
|
||||
// Log the page URL for debugging
|
||||
console.log('Page URL:', page.url);
|
||||
|
||||
return {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
|
@@ -10,4 +10,4 @@ MINIO_REGION="sa-east-1"
|
||||
MINIO_BUCKET_NAME="files"
|
||||
PORT=3333
|
||||
SERVER_IP="localhost"
|
||||
|
||||
MAX_FILESIZE="1073741824"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { prisma } from "../src/shared/prisma";
|
||||
import bcrypt from "bcryptjs";
|
||||
import crypto from "node:crypto";
|
||||
import { env } from '../src/env';
|
||||
|
||||
const defaultConfigs = [
|
||||
// General Configurations
|
||||
@@ -28,10 +28,16 @@ const defaultConfigs = [
|
||||
type: "string",
|
||||
group: "general",
|
||||
},
|
||||
{
|
||||
key: "firstUserAccess",
|
||||
value: "true",
|
||||
type: "boolean",
|
||||
group: "general",
|
||||
},
|
||||
// Storage Configurations
|
||||
{
|
||||
key: "maxFileSize",
|
||||
value: "1073741824", // 1GB in bytes
|
||||
value: env.MAX_FILESIZE, // default 1GiB in bytes - 1073741824
|
||||
type: "bigint",
|
||||
group: "storage",
|
||||
},
|
||||
@@ -114,36 +120,10 @@ const defaultConfigs = [
|
||||
value: "3600",
|
||||
type: "number",
|
||||
group: "security",
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
async function main() {
|
||||
const existingUsers = await prisma.user.count();
|
||||
|
||||
if (existingUsers === 0) {
|
||||
const adminEmail = "admin@example.com";
|
||||
const adminPassword = "admin123";
|
||||
const hashedPassword = await bcrypt.hash(adminPassword, 10);
|
||||
|
||||
const adminUser = await prisma.user.upsert({
|
||||
where: { email: adminEmail },
|
||||
update: {},
|
||||
create: {
|
||||
firstName: "Admin",
|
||||
lastName: "User",
|
||||
username: "admin",
|
||||
email: adminEmail,
|
||||
password: hashedPassword,
|
||||
isAdmin: true,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Admin user seeded:", adminUser);
|
||||
} else {
|
||||
console.log("Users already exist, skipping admin user creation...");
|
||||
}
|
||||
|
||||
console.log("Seeding app configurations...");
|
||||
|
||||
for (const config of defaultConfigs) {
|
||||
|
@@ -3,8 +3,8 @@ import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const userCount = await prisma.user.count();
|
||||
console.log(userCount);
|
||||
const count = await prisma.user.count();
|
||||
console.log(count);
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -14,4 +14,4 @@ main()
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
});
|
@@ -12,6 +12,7 @@ const envSchema = z.object({
|
||||
PORT: z.string().min(1),
|
||||
DATABASE_URL: z.string().min(1),
|
||||
SERVER_IP: z.string().min(1),
|
||||
MAX_FILESIZE: z.string().min(1),
|
||||
});
|
||||
|
||||
export const env = envSchema.parse(process.env);
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { prisma } from "../../shared/prisma";
|
||||
import { AppController } from "./controller";
|
||||
import { ConfigResponseSchema, BulkUpdateConfigSchema } from "./dto";
|
||||
import { FastifyInstance } from "fastify";
|
||||
@@ -8,7 +9,14 @@ export async function appRoutes(app: FastifyInstance) {
|
||||
|
||||
const adminPreValidation = async (request: any, reply: any) => {
|
||||
try {
|
||||
const usersCount = await prisma.user.count();
|
||||
|
||||
if (usersCount <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
await request.jwtVerify();
|
||||
|
||||
if (!request.user.isAdmin) {
|
||||
return reply.status(403).send({
|
||||
error: "Access restricted to administrators",
|
||||
@@ -35,6 +43,7 @@ export async function appRoutes(app: FastifyInstance) {
|
||||
appName: z.string().describe("The application name"),
|
||||
appDescription: z.string().describe("The application description"),
|
||||
appLogo: z.string().describe("The application logo"),
|
||||
firstUserAccess: z.boolean().describe("Whether it's the first user access"),
|
||||
}),
|
||||
400: z.object({ error: z.string().describe("Error message") }),
|
||||
},
|
||||
|
@@ -5,16 +5,18 @@ export class AppService {
|
||||
private configService = new ConfigService();
|
||||
|
||||
async getAppInfo() {
|
||||
const [appName, appDescription, appLogo] = await Promise.all([
|
||||
const [appName, appDescription, appLogo, firstUserAccess] = await Promise.all([
|
||||
this.configService.getValue("appName"),
|
||||
this.configService.getValue("appDescription"),
|
||||
this.configService.getValue("appLogo"),
|
||||
this.configService.getValue("firstUserAccess"),
|
||||
]);
|
||||
|
||||
return {
|
||||
appName,
|
||||
appDescription,
|
||||
appLogo,
|
||||
firstUserAccess : firstUserAccess === "true",
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { prisma } from "../../shared/prisma";
|
||||
import { ConfigService } from "../config/service";
|
||||
import { RegisterFileSchema, RegisterFileInput, UpdateFileSchema } from "./dto";
|
||||
import { RegisterFileSchema, RegisterFileInput, UpdateFileSchema, CheckFileInput, CheckFileSchema } from "./dto";
|
||||
import { FileService } from "./service";
|
||||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
|
||||
@@ -103,6 +103,56 @@ export class FileController {
|
||||
}
|
||||
}
|
||||
|
||||
async checkFile(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
const userId = (request as any).user?.userId;
|
||||
if (!userId) {
|
||||
return reply.status(401).send({
|
||||
error: "Unauthorized: a valid token is required to access this resource.",
|
||||
code: "unauthorized"
|
||||
});
|
||||
}
|
||||
|
||||
const input: CheckFileInput = CheckFileSchema.parse(request.body);
|
||||
|
||||
const maxFileSize = BigInt(await this.configService.getValue("maxFileSize"));
|
||||
if (BigInt(input.size) > maxFileSize) {
|
||||
const maxSizeMB = Number(maxFileSize) / (1024 * 1024);
|
||||
return reply.status(400).send({
|
||||
code: "fileSizeExceeded",
|
||||
error: `File size exceeds the maximum allowed size of ${maxSizeMB}MB`,
|
||||
details: maxSizeMB.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
|
||||
|
||||
const userFiles = await prisma.file.findMany({
|
||||
where: { userId },
|
||||
select: { size: true },
|
||||
});
|
||||
|
||||
const currentStorage = userFiles.reduce((acc, file) => acc + file.size, BigInt(0));
|
||||
|
||||
if (currentStorage + BigInt(input.size) > maxTotalStorage) {
|
||||
const availableSpace = Number(maxTotalStorage - currentStorage) / (1024 * 1024);
|
||||
return reply.status(400).send({
|
||||
error: `Insufficient storage space. You have ${availableSpace.toFixed(2)}MB available`,
|
||||
code: "insufficientStorage",
|
||||
details: availableSpace.toFixed(2),
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(201).send({
|
||||
message: "File checks succeeded.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error in checkFile:", error);
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getDownloadUrl(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const { objectName: encodedObjectName } = request.params as {
|
||||
|
@@ -11,7 +11,19 @@ export const RegisterFileSchema = z.object({
|
||||
objectName: z.string().min(1, "O objectName é obrigatório"),
|
||||
});
|
||||
|
||||
export const CheckFileSchema = z.object({
|
||||
name: z.string().min(1, "O nome do arquivo é obrigatório"),
|
||||
description: z.string().optional(),
|
||||
extension: z.string().min(1, "A extensão é obrigatória"),
|
||||
size: z.number({
|
||||
required_error: "O tamanho é obrigatório",
|
||||
invalid_type_error: "O tamanho deve ser um número",
|
||||
}),
|
||||
objectName: z.string().min(1, "O objectName é obrigatório"),
|
||||
});
|
||||
|
||||
export type RegisterFileInput = z.infer<typeof RegisterFileSchema>;
|
||||
export type CheckFileInput = z.infer<typeof CheckFileSchema>;
|
||||
|
||||
export const UpdateFileSchema = z.object({
|
||||
name: z.string().optional().describe("The file name"),
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { FileController } from "./controller";
|
||||
import { RegisterFileSchema, UpdateFileSchema } from "./dto";
|
||||
import { CheckFileSchema, RegisterFileSchema, UpdateFileSchema } from "./dto";
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -73,6 +73,33 @@ export async function fileRoutes(app: FastifyInstance) {
|
||||
},
|
||||
fileController.registerFile.bind(fileController)
|
||||
);
|
||||
app.post(
|
||||
"/files/check",
|
||||
{
|
||||
schema: {
|
||||
tags: ["File"],
|
||||
operationId: "checkFile",
|
||||
summary: "Check File validity",
|
||||
description: "Checks if the file meets all requirements",
|
||||
body: CheckFileSchema,
|
||||
response: {
|
||||
201: z.object({
|
||||
message: z.string().describe("The file check success message"),
|
||||
}),
|
||||
400: z.object({
|
||||
error: z.string().describe("Error message"),
|
||||
code: z.string().optional().describe("Error code"),
|
||||
details: z.string().optional().describe("Error details"),
|
||||
}),
|
||||
401: z.object({
|
||||
error: z.string().describe("Error message"),
|
||||
code: z.string().optional().describe("Error code"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
fileController.checkFile.bind(fileController)
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/files/:objectName/download",
|
||||
|
@@ -23,6 +23,7 @@ export const createRegisterUserSchema = async () => {
|
||||
|
||||
export type RegisterUserInput = BaseRegisterUserInput & {
|
||||
password: string;
|
||||
isAdmin?: boolean;
|
||||
};
|
||||
|
||||
export const UpdateUserSchema = z.object({
|
||||
|
@@ -24,6 +24,7 @@ export class PrismaUserRepository implements IUserRepository {
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
image: data.image,
|
||||
isAdmin: data.isAdmin,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { prisma } from "../../shared/prisma";
|
||||
import { createPasswordSchema } from "../auth/dto";
|
||||
import { UserController } from "./controller";
|
||||
import { UpdateUserSchema, UserResponseSchema } from "./dto";
|
||||
@@ -10,12 +11,16 @@ export async function userRoutes(app: FastifyInstance) {
|
||||
|
||||
const preValidation = async (request: any, reply: any) => {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
if (!request.user.isAdmin) {
|
||||
return reply
|
||||
.status(403)
|
||||
.send({ error: "Access restricted to administrators" })
|
||||
.description("Access restricted to administrators");
|
||||
const usersCount = await prisma.user.count();
|
||||
|
||||
if (usersCount > 0) {
|
||||
await request.jwtVerify();
|
||||
if (!request.user.isAdmin) {
|
||||
return reply
|
||||
.status(403)
|
||||
.send({ error: "Access restricted to administrators" })
|
||||
.description("Access restricted to administrators");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
@@ -29,11 +29,16 @@ export class UserService {
|
||||
throw new Error("User with this username already exists");
|
||||
}
|
||||
|
||||
const usersCount = await prisma.user.count();
|
||||
const isAdmin = usersCount === 0;
|
||||
|
||||
const hashedPassword = await bcrypt.hash(data.password, 10);
|
||||
const user = await this.userRepository.createUser({
|
||||
...data,
|
||||
password: hashedPassword,
|
||||
isAdmin,
|
||||
});
|
||||
|
||||
return UserResponseSchema.parse(user);
|
||||
}
|
||||
|
||||
|
1
apps/web/.env.example
Normal file
1
apps/web/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
API_BASE_URL=http:localhost:3333
|
2
apps/web/.gitignore
vendored
2
apps/web/.gitignore
vendored
@@ -31,7 +31,7 @@ yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
@@ -260,6 +260,28 @@
|
||||
"notifySuccess": "تم إعلام المستلمين بنجاح",
|
||||
"notifyError": "فشل في إعلام المستلمين"
|
||||
},
|
||||
"register": {
|
||||
"validation": {
|
||||
"firstNameRequired": "الاسم الأول مطلوب",
|
||||
"lastNameRequired": "اسم العائلة مطلوب",
|
||||
"usernameMinLength": "يجب أن يحتوي اسم المستخدم على 3 أحرف على الأقل",
|
||||
"invalidEmail": "البريد الإلكتروني غير صالح",
|
||||
"passwordMinLength": "يجب أن تحتوي كلمة المرور على 8 أحرف على الأقل",
|
||||
"success": "تم إنشاء مستخدم المسؤول بنجاح!",
|
||||
"error": "خطأ في إنشاء مستخدم المسؤول"
|
||||
},
|
||||
"labels": {
|
||||
"firstName": "الاسم الأول",
|
||||
"lastName": "اسم العائلة",
|
||||
"username": "اسم المستخدم",
|
||||
"email": "البريد الإلكتروني",
|
||||
"password": "كلمة المرور"
|
||||
},
|
||||
"buttons": {
|
||||
"creating": "جاري الإنشاء...",
|
||||
"createAdmin": "إنشاء حساب المسؤول"
|
||||
}
|
||||
},
|
||||
"resetPassword": {
|
||||
"pageTitle": "إعادة تعيين كلمة المرور",
|
||||
"header": {
|
||||
@@ -558,7 +580,10 @@
|
||||
"uploadProgress": "تقدم الرفع",
|
||||
"upload": "رفع",
|
||||
"success": "تم رفع الملف بنجاح",
|
||||
"error": "فشل في رفع الملف"
|
||||
"error": "فشل في رفع الملف",
|
||||
"fileSizeExceeded": "حجم الملف يتجاوز الحد المسموح به وهو {{maxsizemb}} ميجابايت.",
|
||||
"insufficientStorage": "مساحة التخزين غير كافية. لديك {{availablespace}} ميجابايت متاحة.",
|
||||
"unauthorized": "غير مصرح به: مطلوب رمز مميز صالح للوصول إلى هذا المورد."
|
||||
},
|
||||
"users": {
|
||||
"modes": {
|
||||
|
@@ -260,6 +260,28 @@
|
||||
"notifySuccess": "Empfänger erfolgreich benachrichtigt",
|
||||
"notifyError": "Fehler beim Benachrichtigen der Empfänger"
|
||||
},
|
||||
"register": {
|
||||
"validation": {
|
||||
"firstNameRequired": "Vorname ist erforderlich",
|
||||
"lastNameRequired": "Nachname ist erforderlich",
|
||||
"usernameMinLength": "Benutzername muss mindestens 3 Zeichen lang sein",
|
||||
"invalidEmail": "Ungültige E-Mail-Adresse",
|
||||
"passwordMinLength": "Passwort muss mindestens 8 Zeichen lang sein",
|
||||
"success": "Administratorbenutzer erfolgreich erstellt!",
|
||||
"error": "Fehler beim Erstellen von Administratorbenutzer"
|
||||
},
|
||||
"labels": {
|
||||
"firstName": "Vorname",
|
||||
"lastName": "Nachname",
|
||||
"username": "Benutzername",
|
||||
"email": "E-Mail",
|
||||
"password": "Passwort"
|
||||
},
|
||||
"buttons": {
|
||||
"creating": "Wird erstellt...",
|
||||
"createAdmin": "Administratorkonto erstellen"
|
||||
}
|
||||
},
|
||||
"resetPassword": {
|
||||
"pageTitle": "Passwort zurücksetzen",
|
||||
"header": {
|
||||
@@ -558,7 +580,10 @@
|
||||
"uploadProgress": "Upload-Fortschritt",
|
||||
"upload": "Hochladen",
|
||||
"success": "Datei erfolgreich hochgeladen",
|
||||
"error": "Fehler beim Hochladen der Datei"
|
||||
"error": "Fehler beim Hochladen der Datei",
|
||||
"fileSizeExceeded": "Dateigröße überschreitet das limit von {maxsizemb}MB.",
|
||||
"insufficientStorage": "Nicht genügend Speicherplatz. Es sind {availablespace}MB verfügbar.",
|
||||
"unauthorized": "Nicht autorisiert: Ein gültiger Token ist erforderlich, um auf diese Ressource zuzugreifen."
|
||||
},
|
||||
"users": {
|
||||
"modes": {
|
||||
|
@@ -260,6 +260,28 @@
|
||||
"notifySuccess": "Recipients notified successfully",
|
||||
"notifyError": "Failed to notify recipients"
|
||||
},
|
||||
"register": {
|
||||
"validation": {
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
"usernameMinLength": "Username must be at least 3 characters",
|
||||
"invalidEmail": "Invalid email",
|
||||
"passwordMinLength": "Password must be at least 8 characters",
|
||||
"success": "Administrator user created successfully!",
|
||||
"error": "Error creating administrator user"
|
||||
},
|
||||
"labels": {
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"username": "Username",
|
||||
"email": "Email",
|
||||
"password": "Password"
|
||||
},
|
||||
"buttons": {
|
||||
"creating": "Creating...",
|
||||
"createAdmin": "Create Admin Account"
|
||||
}
|
||||
},
|
||||
"resetPassword": {
|
||||
"pageTitle": "Reset Password",
|
||||
"header": {
|
||||
@@ -558,7 +580,10 @@
|
||||
"uploadProgress": "Upload progress",
|
||||
"upload": "Upload",
|
||||
"success": "File uploaded successfully",
|
||||
"error": "Failed to upload file"
|
||||
"error": "Failed to upload file",
|
||||
"fileSizeExceeded": "File size exceeds the limit of {maxsizemb}MB.",
|
||||
"insufficientStorage": "Insufficient storage space. You have {availablespace}MB available.",
|
||||
"unauthorized": "Unauthorized: a valid token is required to access this resource."
|
||||
},
|
||||
"users": {
|
||||
"modes": {
|
||||
@@ -640,4 +665,4 @@
|
||||
"emailRequired": "Email is required",
|
||||
"passwordRequired": "Password is required"
|
||||
}
|
||||
}
|
||||
}
|
@@ -260,6 +260,28 @@
|
||||
"notifySuccess": "Destinatarios notificados exitosamente",
|
||||
"notifyError": "Error al notificar a los destinatarios"
|
||||
},
|
||||
"register": {
|
||||
"validation": {
|
||||
"firstNameRequired": "El nombre es obligatorio",
|
||||
"lastNameRequired": "El apellido es obligatorio",
|
||||
"usernameMinLength": "El nombre de usuario debe tener al menos 3 caracteres",
|
||||
"invalidEmail": "Correo electrónico inválido",
|
||||
"passwordMinLength": "La contraseña debe tener al menos 8 caracteres",
|
||||
"success": "¡El usuario del administrador creado con éxito!",
|
||||
"error": "Error a crear usuario administrador"
|
||||
},
|
||||
"labels": {
|
||||
"firstName": "Nombre",
|
||||
"lastName": "Apellido",
|
||||
"username": "Nombre de usuario",
|
||||
"email": "Correo electrónico",
|
||||
"password": "Contraseña"
|
||||
},
|
||||
"buttons": {
|
||||
"creating": "Creando...",
|
||||
"createAdmin": "Crear cuenta de administrador"
|
||||
}
|
||||
},
|
||||
"resetPassword": {
|
||||
"pageTitle": "Restablecer contraseña",
|
||||
"header": {
|
||||
@@ -558,7 +580,10 @@
|
||||
"uploadProgress": "Progreso de la subida",
|
||||
"upload": "Subir",
|
||||
"success": "Archivo subido exitosamente",
|
||||
"error": "Error al subir el archivo"
|
||||
"error": "Error al subir el archivo",
|
||||
"fileSizeExceeded": "El tamaño del archivo excede el límite de {{maxsizemb}}MB.",
|
||||
"insufficientStorage": "Espacio de almacenamiento insuficiente. Tiene {{availablespace}}MB disponibles.",
|
||||
"unauthorized": "No autorizado: se requiere un token válido para acceder a este recurso."
|
||||
},
|
||||
"users": {
|
||||
"modes": {
|
||||
|
@@ -260,6 +260,28 @@
|
||||
"notifySuccess": "Destinataires notifiés avec succès",
|
||||
"notifyError": "Échec de la notification des destinataires"
|
||||
},
|
||||
"register": {
|
||||
"validation": {
|
||||
"firstNameRequired": "Le prénom est requis",
|
||||
"lastNameRequired": "Le nom est requis",
|
||||
"usernameMinLength": "Le nom d'utilisateur doit contenir au moins 3 caractères",
|
||||
"invalidEmail": "Email invalide",
|
||||
"passwordMinLength": "Le mot de passe doit contenir au moins 8 caractères",
|
||||
"success": "L'utilisateur de l'administrateur a créé avec succès!",
|
||||
"error": "Erreur créant l'utilisateur de l'administrateur"
|
||||
},
|
||||
"labels": {
|
||||
"firstName": "Prénom",
|
||||
"lastName": "Nom",
|
||||
"username": "Nom d'utilisateur",
|
||||
"email": "Email",
|
||||
"password": "Mot de passe"
|
||||
},
|
||||
"buttons": {
|
||||
"creating": "Création en cours...",
|
||||
"createAdmin": "Créer un Compte Administrateur"
|
||||
}
|
||||
},
|
||||
"resetPassword": {
|
||||
"pageTitle": "Réinitialiser le Mot de Passe",
|
||||
"header": {
|
||||
@@ -557,7 +579,10 @@
|
||||
"uploadProgress": "Progression de l'envoi",
|
||||
"upload": "Envoyer",
|
||||
"success": "Fichier envoyé avec succès",
|
||||
"error": "Échec de l'envoi du fichier"
|
||||
"error": "Échec de l'envoi du fichier",
|
||||
"fileSizeExceeded": "La taille du fichier dépasse la limite de {{maxsizemb}} Mo.",
|
||||
"insufficientStorage": "Espace de stockage insuffisant. Vous disposez de {{availablespace}} Mo.",
|
||||
"unauthorized": "Non autorisé : un jeton valide est requis pour accéder à cette ressource."
|
||||
},
|
||||
"users": {
|
||||
"modes": {
|
||||
|
@@ -260,6 +260,28 @@
|
||||
"notifySuccess": "प्राप्तकर्ताओं को सफलतापूर्वक सूचित किया गया",
|
||||
"notifyError": "प्राप्तकर्ताओं को सूचित करने में विफल"
|
||||
},
|
||||
"register": {
|
||||
"validation": {
|
||||
"firstNameRequired": "पहला नाम आवश्यक है",
|
||||
"lastNameRequired": "अंतिम नाम आवश्यक है",
|
||||
"usernameMinLength": "उपयोगकर्ता नाम कम से कम 3 अक्षर का होना चाहिए",
|
||||
"invalidEmail": "अमान्य ईमेल",
|
||||
"passwordMinLength": "पासवर्ड कम से कम 8 अक्षर का होना चाहिए",
|
||||
"success": "व्यवस्थापक उपयोगकर्ता ने सफलतापूर्वक बनाया!",
|
||||
"error": "व्यवस्थापक उपयोगकर्ता बनाने में त्रुटि"
|
||||
},
|
||||
"labels": {
|
||||
"firstName": "पहला नाम",
|
||||
"lastName": "अंतिम नाम",
|
||||
"username": "उपयोगकर्ता नाम",
|
||||
"email": "ईमेल",
|
||||
"password": "पासवर्ड"
|
||||
},
|
||||
"buttons": {
|
||||
"creating": "बनाया जा रहा है...",
|
||||
"createAdmin": "व्यवस्थापक खाता बनाएं"
|
||||
}
|
||||
},
|
||||
"resetPassword": {
|
||||
"pageTitle": "पासवर्ड रीसेट करें",
|
||||
"header": {
|
||||
@@ -558,7 +580,10 @@
|
||||
"uploadProgress": "अपलोड प्रगति",
|
||||
"upload": "अपलोड करें",
|
||||
"success": "फाइल सफलतापूर्वक अपलोड हुई",
|
||||
"error": "फाइल अपलोड करने में विफल"
|
||||
"error": "फाइल अपलोड करने में विफल",
|
||||
"fileSizeExceeded": "फ़ाइल का आकार {{maxsizemb}}MB की सीमा से अधिक है।",
|
||||
"insufficientStorage": "अपर्याप्त संग्रहण स्थान। आपके पास {{availablespace}}MB उपलब्ध है।",
|
||||
"unauthorized": "अनधिकृत: इस संसाधन तक पहुँचने के लिए एक मान्य टोकन आवश्यक है।"
|
||||
},
|
||||
"users": {
|
||||
"modes": {
|
||||
|
@@ -260,6 +260,28 @@
|
||||
"notifySuccess": "受信者に正常に通知しました",
|
||||
"notifyError": "受信者への通知に失敗しました"
|
||||
},
|
||||
"register": {
|
||||
"validation": {
|
||||
"firstNameRequired": "名前は必須です",
|
||||
"lastNameRequired": "姓は必須です",
|
||||
"usernameMinLength": "ユーザー名は3文字以上である必要があります",
|
||||
"invalidEmail": "無効なメールアドレスです",
|
||||
"passwordMinLength": "パスワードは8文字以上である必要があります",
|
||||
"success": "管理者ユーザーは正常に作成されました!",
|
||||
"error": "管理者ユーザーの作成エラー"
|
||||
},
|
||||
"labels": {
|
||||
"firstName": "名",
|
||||
"lastName": "姓",
|
||||
"username": "ユーザー名",
|
||||
"email": "メールアドレス",
|
||||
"password": "パスワード"
|
||||
},
|
||||
"buttons": {
|
||||
"creating": "作成中...",
|
||||
"createAdmin": "管理者アカウントを作成"
|
||||
}
|
||||
},
|
||||
"resetPassword": {
|
||||
"pageTitle": "パスワードリセット",
|
||||
"header": {
|
||||
@@ -558,7 +580,10 @@
|
||||
"uploadProgress": "アップロードの進行状況",
|
||||
"upload": "アップロード",
|
||||
"success": "ファイルが正常にアップロードされました",
|
||||
"error": "ファイルのアップロードに失敗しました"
|
||||
"error": "ファイルのアップロードに失敗しました",
|
||||
"fileSizeExceeded": "ファイルサイズが制限値 {{maxsizemb}}MB を超えています。",
|
||||
"insufficientStorage": "ストレージ容量が不足しています。利用可能な容量は {{availablespace}}MB です。",
|
||||
"unauthorized": "権限がありません: このリソースにアクセスするには有効なトークンが必要です。"
|
||||
},
|
||||
"users": {
|
||||
"modes": {
|
||||
|
@@ -260,6 +260,28 @@
|
||||
"notifySuccess": "수신자에게 성공적으로 알렸습니다",
|
||||
"notifyError": "수신자 알림 전송에 실패했습니다"
|
||||
},
|
||||
"register": {
|
||||
"validation": {
|
||||
"firstNameRequired": "이름을 입력해주세요",
|
||||
"lastNameRequired": "성을 입력해주세요",
|
||||
"usernameMinLength": "사용자 이름은 최소 3자 이상이어야 합니다",
|
||||
"invalidEmail": "유효하지 않은 이메일입니다",
|
||||
"passwordMinLength": "비밀번호는 최소 8자 이상이어야 합니다",
|
||||
"success": "관리자 사용자가 성공적으로 생성되었습니다!",
|
||||
"error": "오류 관리자 사용자 생성"
|
||||
},
|
||||
"labels": {
|
||||
"firstName": "이름",
|
||||
"lastName": "성",
|
||||
"username": "사용자 이름",
|
||||
"email": "이메일",
|
||||
"password": "비밀번호"
|
||||
},
|
||||
"buttons": {
|
||||
"creating": "생성 중...",
|
||||
"createAdmin": "관리자 계정 생성"
|
||||
}
|
||||
},
|
||||
"resetPassword": {
|
||||
"pageTitle": "비밀번호 재설정",
|
||||
"header": {
|
||||
@@ -558,7 +580,10 @@
|
||||
"uploadProgress": "업로드 진행",
|
||||
"upload": "업로드",
|
||||
"success": "파일이 성공적으로 업로드되었습니다",
|
||||
"error": "파일 업로드에 실패했습니다"
|
||||
"error": "파일 업로드에 실패했습니다",
|
||||
"fileSizeExceeded": "파일 크기가 {{maxsizemb}}MB 제한을 초과합니다.",
|
||||
"insufficientStorage": "저장 공간이 부족합니다. {{availablespace}}MB의 사용 가능한 공간이 있습니다.",
|
||||
"unauthorized": "권한 없음: 이 리소스에 액세스하려면 유효한 토큰이 필요합니다."
|
||||
},
|
||||
"users": {
|
||||
"modes": {
|
||||
|
@@ -260,6 +260,28 @@
|
||||
"notifySuccess": "Destinatários notificados com sucesso",
|
||||
"notifyError": "Falha ao notificar destinatários"
|
||||
},
|
||||
"register": {
|
||||
"validation": {
|
||||
"firstNameRequired": "Nome é obrigatório",
|
||||
"lastNameRequired": "Sobrenome é obrigatório",
|
||||
"usernameMinLength": "Nome de usuário deve ter no mínimo 3 caracteres",
|
||||
"invalidEmail": "Email inválido",
|
||||
"passwordMinLength": "Senha deve ter no mínimo 8 caracteres",
|
||||
"success": "Usuário do administrador criado com sucesso!",
|
||||
"error": "Erro a criação de usuário do administrador"
|
||||
},
|
||||
"labels": {
|
||||
"firstName": "Nome",
|
||||
"lastName": "Sobrenome",
|
||||
"username": "Nome de Usuário",
|
||||
"email": "Email",
|
||||
"password": "Senha"
|
||||
},
|
||||
"buttons": {
|
||||
"creating": "Criando conta...",
|
||||
"createAdmin": "Criar conta de administrador"
|
||||
}
|
||||
},
|
||||
"resetPassword": {
|
||||
"pageTitle": "Redefinir Senha",
|
||||
"header": {
|
||||
@@ -558,7 +580,10 @@
|
||||
"uploadProgress": "Progresso do upload",
|
||||
"upload": "Enviar",
|
||||
"success": "Arquivo enviado com sucesso",
|
||||
"error": "Falha ao enviar arquivo"
|
||||
"error": "Falha ao enviar arquivo",
|
||||
"fileSizeExceeded": "O tamanho do arquivo excede o limite de {{maxsizemb}}MB.",
|
||||
"insufficientStorage": "Espaço de armazenamento insuficiente. Você tem {{availablespace}}MB disponíveis.",
|
||||
"unauthorized": "Não autorizado: um token válido é necessário para acessar este recurso."
|
||||
},
|
||||
"users": {
|
||||
"modes": {
|
||||
|
@@ -260,6 +260,28 @@
|
||||
"notifySuccess": "Получатели успешно уведомлены",
|
||||
"notifyError": "Не удалось уведомить получателей"
|
||||
},
|
||||
"register": {
|
||||
"validation": {
|
||||
"firstNameRequired": "Имя обязательно",
|
||||
"lastNameRequired": "Фамилия обязательна",
|
||||
"usernameMinLength": "Имя пользователя должно содержать минимум 3 символа",
|
||||
"invalidEmail": "Неверный email",
|
||||
"passwordMinLength": "Пароль должен содержать минимум 8 символов",
|
||||
"success": "Пользователь администратора создал успешно!",
|
||||
"error": "Ошибка создания пользователя администратора"
|
||||
},
|
||||
"labels": {
|
||||
"firstName": "Имя",
|
||||
"lastName": "Фамилия",
|
||||
"username": "Имя пользователя",
|
||||
"email": "Email",
|
||||
"password": "Пароль"
|
||||
},
|
||||
"buttons": {
|
||||
"creating": "Создание...",
|
||||
"createAdmin": "Создать учетную запись администратора"
|
||||
}
|
||||
},
|
||||
"resetPassword": {
|
||||
"pageTitle": "Сбросить пароль",
|
||||
"header": {
|
||||
@@ -558,7 +580,10 @@
|
||||
"uploadProgress": "Прогресс загрузки",
|
||||
"upload": "Загрузить",
|
||||
"success": "Файл успешно загружен",
|
||||
"error": "Не удалось загрузить файл"
|
||||
"error": "Не удалось загрузить файл",
|
||||
"fileSizeExceeded": "Размер файла превышает лимит в {{maxsizemb}}МБ.",
|
||||
"insufficientStorage": "Недостаточно места для хранения. У вас доступно {{availablespace}}МБ.",
|
||||
"unauthorized": "Не авторизовано: для доступа к этому ресурсу требуется действительный токен."
|
||||
},
|
||||
"users": {
|
||||
"modes": {
|
||||
|
@@ -260,6 +260,28 @@
|
||||
"notifySuccess": "Alıcılar başarıyla bildirildi",
|
||||
"notifyError": "Alıcılara bildirim gönderilemedi"
|
||||
},
|
||||
"register": {
|
||||
"validation": {
|
||||
"firstNameRequired": "Ad gereklidir",
|
||||
"lastNameRequired": "Soyad gereklidir",
|
||||
"usernameMinLength": "Kullanıcı adı en az 3 karakter olmalıdır",
|
||||
"invalidEmail": "Geçersiz e-posta",
|
||||
"passwordMinLength": "Şifre en az 8 karakter olmalıdır",
|
||||
"success": "Yönetici kullanıcısı başarıyla yarattı!",
|
||||
"error": "Yönetici kullanıcısı oluşturma hatası"
|
||||
},
|
||||
"labels": {
|
||||
"firstName": "Ad",
|
||||
"lastName": "Soyad",
|
||||
"username": "Kullanıcı Adı",
|
||||
"email": "E-posta",
|
||||
"password": "Şifre"
|
||||
},
|
||||
"buttons": {
|
||||
"creating": "Oluşturuluyor...",
|
||||
"createAdmin": "Yönetici Hesabı Oluştur"
|
||||
}
|
||||
},
|
||||
"resetPassword": {
|
||||
"pageTitle": "Şifreyi Sıfırla",
|
||||
"header": {
|
||||
@@ -558,7 +580,10 @@
|
||||
"uploadProgress": "Yükleme İlerlemesi",
|
||||
"upload": "Yükle",
|
||||
"success": "Dosya başarıyla yüklendi",
|
||||
"error": "Dosya yüklenemedi"
|
||||
"error": "Dosya yüklenemedi",
|
||||
"fileSizeExceeded": "Dosya boyutu {{maxsizemb}}MB sınırını aşıyor.",
|
||||
"insufficientStorage": "Yetersiz depolama alanı. {{availablespace}}MB kullanılabilir alanınız var.",
|
||||
"unauthorized": "Yetkisiz: Bu kaynağa erişmek için geçerli bir token gereklidir."
|
||||
},
|
||||
"users": {
|
||||
"modes": {
|
||||
|
@@ -260,6 +260,28 @@
|
||||
"notifySuccess": "收件人通知成功",
|
||||
"notifyError": "通知收件人失败"
|
||||
},
|
||||
"register": {
|
||||
"validation": {
|
||||
"firstNameRequired": "名字为必填项",
|
||||
"lastNameRequired": "姓氏为必填项",
|
||||
"usernameMinLength": "用户名至少需要3个字符",
|
||||
"invalidEmail": "无效的电子邮件",
|
||||
"passwordMinLength": "密码至少需要8个字符",
|
||||
"success": "管理员用户成功创建了!",
|
||||
"error": "错误创建管理员用户"
|
||||
},
|
||||
"labels": {
|
||||
"firstName": "名字",
|
||||
"lastName": "姓氏",
|
||||
"username": "用户名",
|
||||
"email": "电子邮件",
|
||||
"password": "密码"
|
||||
},
|
||||
"buttons": {
|
||||
"creating": "正在创建...",
|
||||
"createAdmin": "创建管理员账户"
|
||||
}
|
||||
},
|
||||
"resetPassword": {
|
||||
"pageTitle": "重置密码",
|
||||
"header": {
|
||||
@@ -558,7 +580,10 @@
|
||||
"uploadProgress": "上传进度",
|
||||
"upload": "上传",
|
||||
"success": "文件上传成功",
|
||||
"error": "文件上传失败"
|
||||
"error": "文件上传失败",
|
||||
"fileSizeExceeded": "文件大小超出 {{maxsizemb}}MB 的限制。",
|
||||
"insufficientStorage": "存储空间不足。您有 {{availablespace}}MB 可用空间。",
|
||||
"unauthorized": "未授权:访问此资源需要有效的令牌。"
|
||||
},
|
||||
"users": {
|
||||
"modes": {
|
||||
|
@@ -12,15 +12,15 @@ const nextConfig: NextConfig = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '**',
|
||||
protocol: "https",
|
||||
hostname: "**",
|
||||
},
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: '**',
|
||||
protocol: "http",
|
||||
hostname: "**",
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
@@ -1,9 +1,6 @@
|
||||
import { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { getAllConfigs } from "@/http/endpoints";
|
||||
import { Config } from "@/types/layout";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
@@ -11,12 +8,8 @@ interface LayoutProps {
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations();
|
||||
|
||||
const response = await getAllConfigs();
|
||||
const appNameConfig = response.data.configs.find((config: Config) => config.key === "appName");
|
||||
const appName = appNameConfig?.value || "Palmr";
|
||||
|
||||
return {
|
||||
title: `${t("share.pageTitle")} | ${appName}`,
|
||||
title: `${t("share.pageTitle")}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -24,7 +24,5 @@ export async function GET(req: NextRequest) {
|
||||
res.headers.set("Set-Cookie", setCookie.join(","));
|
||||
}
|
||||
|
||||
console.log(res);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function PATCH(req: NextRequest, { params }: { params: { key: string } }) {
|
||||
const { key } = params;
|
||||
const key = params.key;
|
||||
const body = await req.text();
|
||||
const cookieHeader = req.headers.get("cookie");
|
||||
|
||||
|
32
apps/web/src/app/api/(proxy)/files/check/route.ts
Normal file
32
apps/web/src/app/api/(proxy)/files/check/route.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.text();
|
||||
const cookieHeader = req.headers.get("cookie");
|
||||
|
||||
const apiRes = await fetch(`${process.env.API_BASE_URL}/files/check`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
cookie: cookieHeader || "",
|
||||
},
|
||||
body,
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
const resBody = await apiRes.text();
|
||||
|
||||
const res = new NextResponse(resBody, {
|
||||
status: apiRes.status,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const setCookie = apiRes.headers.getSetCookie?.() || [];
|
||||
if (setCookie.length > 0) {
|
||||
res.headers.set("Set-Cookie", setCookie.join(","));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
@@ -2,16 +2,15 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function GET(req: NextRequest, { params }: { params: { alias: string } }) {
|
||||
const cookieHeader = req.headers.get("cookie");
|
||||
const queryParams = new URLSearchParams(req.url.split("?")[1]) || undefined;
|
||||
|
||||
const apiRes = await fetch(`${process.env.API_BASE_URL}/shares/alias/${params.alias}`, {
|
||||
const url = new URL(req.url);
|
||||
const queryParams = url.search;
|
||||
const apiRes = await fetch(`${process.env.API_BASE_URL}/shares/alias/${params.alias}${queryParams}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
cookie: cookieHeader || "",
|
||||
},
|
||||
redirect: "manual",
|
||||
...(queryParams ? { params: queryParams } : {}),
|
||||
});
|
||||
|
||||
const resBody = await apiRes.text();
|
||||
@@ -29,4 +28,4 @@ export async function GET(req: NextRequest, { params }: { params: { alias: strin
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
@@ -8,7 +8,6 @@ import { formatStorageSize } from "../utils/format-storage-size";
|
||||
|
||||
export function StorageUsage({ diskSpace }: StorageUsageProps) {
|
||||
const t = useTranslations();
|
||||
console.log("diskSpace:", diskSpace);
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
|
@@ -4,7 +4,7 @@ import { useTranslations } from "next-intl";
|
||||
|
||||
import { useAppInfo } from "@/contexts/app-info-context";
|
||||
|
||||
export function LoginHeader() {
|
||||
export function LoginHeader({ firstAccess }: { firstAccess: boolean }) {
|
||||
const t = useTranslations();
|
||||
const { appName, refreshAppInfo } = useAppInfo();
|
||||
|
||||
@@ -22,7 +22,7 @@ export function LoginHeader() {
|
||||
<h1 className="text-2xl font-semibold tracking-tight text-center">
|
||||
{t("login.welcome")} {appName}
|
||||
</h1>
|
||||
<p className="text-default-500 text-sm">{t("login.signInToContinue")}</p>
|
||||
{!firstAccess && <p className="text-default-500 text-sm">{t("login.signInToContinue")}</p>}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
148
apps/web/src/app/login/components/register-form.tsx
Normal file
148
apps/web/src/app/login/components/register-form.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useAppInfo } from "@/contexts/app-info-context";
|
||||
import { registerUser, updateConfig } from "@/http/endpoints";
|
||||
import { PasswordVisibilityToggle } from "./password-visibility-toggle";
|
||||
|
||||
interface RegisterFormProps {
|
||||
isVisible: boolean;
|
||||
onToggleVisibility: () => void;
|
||||
}
|
||||
|
||||
export function RegisterForm({ isVisible, onToggleVisibility }: RegisterFormProps) {
|
||||
const t = useTranslations();
|
||||
const { refreshAppInfo } = useAppInfo();
|
||||
|
||||
const registerSchema = z.object({
|
||||
firstName: z.string().min(1, t("register.validation.firstNameRequired")),
|
||||
lastName: z.string().min(1, t("register.validation.lastNameRequired")),
|
||||
username: z.string().min(3, t("register.validation.usernameMinLength")),
|
||||
email: z.string().email(t("register.validation.invalidEmail")),
|
||||
password: z.string().min(8, t("register.validation.passwordMinLength")),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof registerSchema>>({
|
||||
resolver: zodResolver(registerSchema),
|
||||
defaultValues: {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof registerSchema>) => {
|
||||
try {
|
||||
await registerUser({
|
||||
...data,
|
||||
});
|
||||
|
||||
await updateConfig("firstUserAccess", {
|
||||
value: "false",
|
||||
});
|
||||
|
||||
await refreshAppInfo();
|
||||
toast.success(t("register.validation.success"));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error(t("register.validation.error"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="firstName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("register.labels.firstName")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} className="bg-transparent backdrop-blur-md" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("register.labels.lastName")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} className="bg-transparent backdrop-blur-md" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("register.labels.username")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} className="bg-transparent backdrop-blur-md" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("register.labels.email")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="email" className="bg-transparent backdrop-blur-md" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("register.labels.password")}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
{...field}
|
||||
type={isVisible ? "text" : "password"}
|
||||
className="bg-transparent backdrop-blur-md pr-10"
|
||||
/>
|
||||
<PasswordVisibilityToggle isVisible={isVisible} onToggle={onToggleVisibility} />
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button className="w-full mt-4" type="submit" disabled={form.formState.isSubmitting}>
|
||||
{form.formState.isSubmitting ? t("register.buttons.creating") : t("register.buttons.createAdmin")}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
@@ -5,13 +5,16 @@ import { motion } from "framer-motion";
|
||||
import { LanguageSwitcher } from "@/components/general/language-switcher";
|
||||
import { LoadingScreen } from "@/components/layout/loading-screen";
|
||||
import { DefaultFooter } from "@/components/ui/default-footer";
|
||||
import { useAppInfo } from "@/contexts/app-info-context";
|
||||
import { LoginForm } from "./components/login-form";
|
||||
import { LoginHeader } from "./components/login-header";
|
||||
import { RegisterForm } from "./components/register-form";
|
||||
import { StaticBackgroundLights } from "./components/static-background-lights";
|
||||
import { useLogin } from "./hooks/use-login";
|
||||
|
||||
export default function LoginPage() {
|
||||
const login = useLogin();
|
||||
const { firstAccess } = useAppInfo();
|
||||
|
||||
if (login.isAuthenticated === null) {
|
||||
return <LoadingScreen />;
|
||||
@@ -32,13 +35,17 @@ export default function LoginPage() {
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<LoginHeader />
|
||||
<LoginForm
|
||||
error={login.error}
|
||||
isVisible={login.isVisible}
|
||||
onSubmit={login.onSubmit}
|
||||
onToggleVisibility={login.toggleVisibility}
|
||||
/>
|
||||
<LoginHeader firstAccess={firstAccess as boolean} />
|
||||
{firstAccess ? (
|
||||
<RegisterForm isVisible={login.isVisible} onToggleVisibility={login.toggleVisibility} />
|
||||
) : (
|
||||
<LoginForm
|
||||
error={login.error}
|
||||
isVisible={login.isVisible}
|
||||
onSubmit={login.onSubmit}
|
||||
onToggleVisibility={login.toggleVisibility}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -4,6 +4,7 @@ import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ProfileFormProps } from "../types";
|
||||
|
||||
export function ProfileForm({ form, onSubmit }: ProfileFormProps) {
|
||||
@@ -23,6 +24,7 @@ export function ProfileForm({ form, onSubmit }: ProfileFormProps) {
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>{t("profile.form.firstName")}</Label>
|
||||
<Input
|
||||
{...register("firstName")}
|
||||
className={errors.firstName ? "border-red-500" : ""}
|
||||
@@ -33,6 +35,7 @@ export function ProfileForm({ form, onSubmit }: ProfileFormProps) {
|
||||
{errors.firstName && <span className="text-sm text-red-500">{errors.firstName.message}</span>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>{t("profile.form.lastName")}</Label>
|
||||
<Input
|
||||
{...register("lastName")}
|
||||
className={errors.lastName ? "border-red-500" : ""}
|
||||
@@ -44,6 +47,7 @@ export function ProfileForm({ form, onSubmit }: ProfileFormProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>{t("profile.form.username")}</Label>
|
||||
<Input
|
||||
{...register("username")}
|
||||
className={errors.username ? "border-red-500" : ""}
|
||||
@@ -54,6 +58,7 @@ export function ProfileForm({ form, onSubmit }: ProfileFormProps) {
|
||||
{errors.username && <span className="text-sm text-red-500">{errors.username.message}</span>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>{t("profile.form.email")}</Label>
|
||||
<Input
|
||||
{...register("email")}
|
||||
type="email"
|
||||
|
@@ -9,8 +9,9 @@ import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { getPresignedUrl, registerFile } from "@/http/endpoints";
|
||||
import { getPresignedUrl, registerFile, checkFile } from "@/http/endpoints";
|
||||
import { generateSafeFileName } from "@/utils/file-utils";
|
||||
import getErrorData from "@/utils/getErrorData";
|
||||
|
||||
interface UploadFileModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -62,12 +63,31 @@ export function UploadFileModal({ isOpen, onClose, onSuccess }: UploadFileModalP
|
||||
if (!selectedFile) return;
|
||||
|
||||
try {
|
||||
setIsUploading(true);
|
||||
setUploadProgress(0);
|
||||
|
||||
const fileName = selectedFile.name;
|
||||
const extension = fileName.split(".").pop() || "";
|
||||
const safeObjectName = generateSafeFileName(fileName);
|
||||
try {
|
||||
await checkFile({
|
||||
name: fileName,
|
||||
objectName: "checkFile",
|
||||
size: selectedFile.size,
|
||||
extension: extension,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("File check failed:", error);
|
||||
const errorData = getErrorData(error);
|
||||
if (errorData.code === "fileSizeExceeded") {
|
||||
toast.error(t(`uploadFile.${errorData.code}`, { maxsizemb: t(`${errorData.details}`) }));
|
||||
} else if (errorData.code === "insufficientStorage") {
|
||||
toast.error(t(`uploadFile.${errorData.code}`, { availablespace: t(`${errorData.details}`) }));
|
||||
}else {
|
||||
toast.error(t(`uploadFile.${errorData.code}`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
setUploadProgress(0);
|
||||
|
||||
const presignedResponse = await getPresignedUrl({
|
||||
filename: safeObjectName.replace(`.${extension}`, ""),
|
||||
|
@@ -5,6 +5,8 @@ import { getAppInfo } from "@/http/endpoints";
|
||||
interface AppInfoStore {
|
||||
appName: string;
|
||||
appLogo: string;
|
||||
firstAccess: boolean | null;
|
||||
isLoading: boolean;
|
||||
setAppName: (name: string) => void;
|
||||
setAppLogo: (logo: string) => void;
|
||||
refreshAppInfo: () => Promise<void>;
|
||||
@@ -15,23 +17,35 @@ const updateTitle = (name: string) => {
|
||||
};
|
||||
|
||||
export const useAppInfo = create<AppInfoStore>((set) => {
|
||||
if (typeof window !== "undefined") {
|
||||
getAppInfo()
|
||||
.then((response) => {
|
||||
const initialState = {
|
||||
appName: "",
|
||||
appLogo: "",
|
||||
firstAccess: null,
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
const loadAppInfo = async () => {
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
const response = await getAppInfo();
|
||||
set({
|
||||
appName: response.data.appName,
|
||||
appLogo: response.data.appLogo,
|
||||
firstAccess: response.data.firstUserAccess,
|
||||
isLoading: false,
|
||||
});
|
||||
updateTitle(response.data.appName);
|
||||
})
|
||||
.catch((error) => {
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch app info:", error);
|
||||
});
|
||||
}
|
||||
set({ isLoading: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadAppInfo();
|
||||
|
||||
return {
|
||||
appName: "",
|
||||
appLogo: "",
|
||||
...initialState,
|
||||
setAppName: (name: string) => {
|
||||
set({ appName: name });
|
||||
updateTitle(name);
|
||||
@@ -40,15 +54,19 @@ export const useAppInfo = create<AppInfoStore>((set) => {
|
||||
set({ appLogo: logo });
|
||||
},
|
||||
refreshAppInfo: async () => {
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const response = await getAppInfo();
|
||||
set({
|
||||
appName: response.data.appName,
|
||||
appLogo: response.data.appLogo,
|
||||
firstAccess: response.data.firstUserAccess,
|
||||
isLoading: false,
|
||||
});
|
||||
updateTitle(response.data.appName);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch app info:", error);
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@@ -9,6 +9,8 @@ import type {
|
||||
ListFilesResult,
|
||||
RegisterFileBody,
|
||||
RegisterFileResult,
|
||||
CheckFileBody,
|
||||
CheckFileResult,
|
||||
UpdateFileBody,
|
||||
UpdateFileResult,
|
||||
} from "./types";
|
||||
@@ -27,6 +29,19 @@ export const getPresignedUrl = <TData = GetPresignedUrlResult>(
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the file meets constraints like MAX_FILESIZE
|
||||
* @summary Check file for constraints
|
||||
*/
|
||||
export const checkFile = <TData = CheckFileResult>(
|
||||
CheckFileBody: CheckFileBody,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<TData> => {
|
||||
return apiInstance.post(`/api/files/check`, CheckFileBody, options);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Registers file metadata in the database
|
||||
* @summary Register File Metadata
|
||||
|
@@ -7,6 +7,8 @@ import type {
|
||||
GetPresignedUrlParams,
|
||||
ListFiles200,
|
||||
RegisterFile201,
|
||||
CheckFile201,
|
||||
CheckFileBody,
|
||||
RegisterFileBody,
|
||||
UpdateFile200,
|
||||
UpdateFileBody,
|
||||
@@ -14,9 +16,10 @@ import type {
|
||||
|
||||
export type GetPresignedUrlResult = AxiosResponse<GetPresignedUrl200>;
|
||||
export type RegisterFileResult = AxiosResponse<RegisterFile201>;
|
||||
export type CheckFileResult = AxiosResponse<CheckFile201>;
|
||||
export type ListFilesResult = AxiosResponse<ListFiles200>;
|
||||
export type GetDownloadUrlResult = AxiosResponse<GetDownloadUrl200>;
|
||||
export type DeleteFileResult = AxiosResponse<DeleteFile200>;
|
||||
export type UpdateFileResult = AxiosResponse<UpdateFile200>;
|
||||
|
||||
export type { GetPresignedUrlParams, RegisterFileBody, UpdateFileBody };
|
||||
export type { GetPresignedUrlParams, RegisterFileBody, UpdateFileBody, CheckFileBody };
|
||||
|
14
apps/web/src/http/models/checkFile201.ts
Normal file
14
apps/web/src/http/models/checkFile201.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Generated by orval v7.5.0 🍺
|
||||
* Do not edit manually.
|
||||
* 🌴 Palmr. API
|
||||
* API documentation for Palmr file sharing system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { CheckFile201File } from "./checkFile201File";
|
||||
|
||||
export type CheckFile201 = {
|
||||
file: CheckFile201File;
|
||||
/** The file check message */
|
||||
message: string;
|
||||
};
|
31
apps/web/src/http/models/checkFile201File.ts
Normal file
31
apps/web/src/http/models/checkFile201File.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Generated by orval v7.5.0 🍺
|
||||
* Do not edit manually.
|
||||
* 🌴 Palmr. API
|
||||
* API documentation for Palmr file sharing system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export type CheckFile201File = {
|
||||
/** The file ID */
|
||||
id: string;
|
||||
/** The file name */
|
||||
name: string;
|
||||
/**
|
||||
* The file description
|
||||
* @nullable
|
||||
*/
|
||||
description: string | null;
|
||||
/** The file extension */
|
||||
extension: string;
|
||||
/** The file size */
|
||||
size: string;
|
||||
/** The object name of the file */
|
||||
objectName: string;
|
||||
/** The user ID */
|
||||
userId: string;
|
||||
/** The file creation date */
|
||||
createdAt: string;
|
||||
/** The file last update date */
|
||||
updatedAt: string;
|
||||
};
|
18
apps/web/src/http/models/checkFileBody.ts
Normal file
18
apps/web/src/http/models/checkFileBody.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Generated by orval v7.5.0 🍺
|
||||
* Do not edit manually.
|
||||
* 🌴 Palmr. API
|
||||
* API documentation for Palmr file sharing system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export type CheckFileBody = {
|
||||
/** @minLength 1 */
|
||||
name: string;
|
||||
description?: string;
|
||||
/** @minLength 1 */
|
||||
extension: string;
|
||||
size: number;
|
||||
/** @minLength 1 */
|
||||
objectName: string;
|
||||
};
|
@@ -13,4 +13,6 @@ export type GetAppInfo200 = {
|
||||
appDescription: string;
|
||||
/** The application logo */
|
||||
appLogo: string;
|
||||
/** The application first Access */
|
||||
firstUserAccess: boolean;
|
||||
};
|
||||
|
@@ -16,7 +16,7 @@ export default getRequestConfig(async ({ locale }) => {
|
||||
messages: (await import(`../../messages/${resolvedLocale}.json`)).default,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
return {
|
||||
locale: DEFAULT_LOCALE,
|
||||
messages: (await import(`../../messages/${DEFAULT_LOCALE}.json`)).default,
|
||||
|
40
apps/web/src/utils/getErrorData.ts
Normal file
40
apps/web/src/utils/getErrorData.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import axios from "axios";
|
||||
|
||||
export interface ErrorData {
|
||||
/**
|
||||
* The specific error code from the backend (e.g., "fileSizeExceeded"),
|
||||
*/
|
||||
code: string;
|
||||
/**
|
||||
* An optional object containing dynamic data from the backend's 'details' field,
|
||||
* used for frontend interpolation (e.g.: 1024 for maxsizemb).
|
||||
*/
|
||||
details?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to extract the specific error 'code' and, if available, 'details' string from an Axios error response.
|
||||
*
|
||||
* @param error The error object caught.
|
||||
* @returns The 'code' and if available 'details' string from error.response.data.[code|details] if found.
|
||||
* If not found, returns a default object with code "error" and details "undefined".
|
||||
*/
|
||||
const getErrorData = (error: unknown): ErrorData => {
|
||||
// Check if it's an Axios error and has a response with data
|
||||
if (
|
||||
axios.isAxiosError(error) &&
|
||||
error.response?.data &&
|
||||
typeof error.response.data.code === 'string' &&
|
||||
error.response.data.code.length > 0 // Ensure it's not an empty string
|
||||
) {
|
||||
// Return the code string
|
||||
const code = error.response.data.code;
|
||||
const details = typeof error.response.data.details === 'string' && error.response.data.details !== null ? error.response.data.details : undefined;
|
||||
return { code, details };
|
||||
}
|
||||
|
||||
// If code invalid, return "error" as code
|
||||
return { code: "error", details: "undefined" };
|
||||
};
|
||||
|
||||
export default getErrorData;
|
@@ -19,6 +19,7 @@ services:
|
||||
- MINIO_BUCKET_NAME=files # MinIO bucket name - This is needed for MinIO to work properly, dont change it if you don't know what you are doing
|
||||
- FRONTEND_URL=${APP_URL:-http://${SERVER_IP:-localhost}:${APP_EXTERNAL_PORT:-5487}} # Frontend URL - Make sure to use the correct frontend URL, depends on where the frontend is running, its prepared for localhost, but you can change it to your frontend URL if needed
|
||||
- SERVER_IP=${SERVER_IP:-localhost} # Server IP - Make sure to use the correct server IP if you running on a cloud provider or a virtual machine. This prepared for localhost, but you can change it to your server IP if needed
|
||||
- MAX_FILESIZE=${MAX_FILESIZE:-1073741824} # Max Filesize for upload - Declared in Bytes. Default is 1GiB
|
||||
ports:
|
||||
- "${API_EXTERNAL_PORT:-3333}:${API_INTERNAL_PORT:-3333}" # Backend port mapping
|
||||
restart: unless-stopped
|
||||
|
Reference in New Issue
Block a user