Compare commits

...

16 Commits

Author SHA1 Message Date
Daniel Luiz Alves
c1baa3a16d v2.0.0-beta.3 (#30) 2025-05-06 10:06:52 -03:00
Daniel Luiz Alves
cfc103e056 fix: (docker images) (#29) 2025-05-06 09:41:12 -03:00
Charly Gley
1a0b565ae0 fix: (docker images)
changed check-db.mjs to output number in stdout
changed routes.ts to import prisma with relative paths
2025-05-05 21:04:14 +02:00
Daniel Luiz Alves
37a30f1bd7 v2.0.0-beta.2 (#28) 2025-05-05 11:42:17 -03:00
Daniel Luiz Alves
d7bdffe096 feat: Pre-upload validation API (#26) 2025-05-05 09:50:44 -03:00
Daniel Luiz Alves
277fa3ce28 fix(register-form) (#27) 2025-05-05 09:50:09 -03:00
Charly Gley
11b2c5d9a1 fix(register-form): change form validation password length to 8 instead of 6 (backend expects at least 8). Added translations for the error/success toast messages. 2025-05-04 04:12:19 +02:00
Charly Gley
0e1ea0f2ef feat: Pre-upload validation API
I implemented a new API Endpoint /files/check that is queried before trying to upload the file to the MinIO backend.
This prevents the current issue where files exceeding the maximum file size would be uploaded to the storage backend but then fail to get registered, resulting in "dead" data invisible in the frontend but stored in the backend.
Additionally I added new toast notifications for the following errors that can happen while checking if the file is valid:
- filesizeExceeded: Displays a toast that the file is too large and shows the current MAX_FILESIZE
- insufficientStorage: Displays a toast with the error that not enough storage is available together with the currently available storage
- unauthorized: Displays a toast saying that the token is invalid
2025-05-03 05:22:57 +02:00
Daniel Luiz Alves
a8c53afe8c [Next Release] v2.0.0-beta.1 (#21) 2025-05-02 18:03:32 -03:00
PunchEnergyFTW
5b3dab7c75 fix(shares): Allow users to set and update passwords for shares (#25) 2025-05-02 17:01:22 -03:00
Daniel Luiz Alves
57d89e5807 Feat: Initial admin creation (#24)
In this pull request, we're removing the default admin user that was previously created via seed. Instead, the initial user will now be created dynamically by whoever is installing Palmr., eliminating the need for a predefined seed. This change makes the setup process more fluid and secure.
2025-05-02 16:23:13 -03:00
PunchEnergyFTW
107a467bcc Feat: Add max filesize environment variable (#20)
Co-authored-by: Charly Gley <charly@gley.dev>
2025-04-27 23:30:18 -03:00
Daniel Luiz Alves
792183bbb3 Merge branch 'main' into next 2025-04-27 23:27:44 -03:00
Daniel Luiz Alves
a12183d4a8 Squashed commit of the following:
commit 359d0a0403
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 25 14:11:09 2025 -0300

    docs(installation): add command to generate .env file

    Add a command to generate a .env file with the `server_ip` configuration to simplify the setup process for users. This command can be executed in the server terminal at the same path as the docker-compose.yaml file.

commit c3967eca72
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 25 13:42:26 2025 -0300

    refactor(web): remove unused config fetch in layout metadata

    Simplify the metadata generation in the layout component by removing the unnecessary fetch of app configurations. This reduces complexity and improves maintainability.

commit 6898dd8d1b
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 25 01:22:53 2025 -0300

    refactor(layout): remove unused changeLayout prop from Banner component

    The changeLayout prop was not utilized in the Banner component, so it has been removed to simplify the code and improve maintainability.

    style(home): add version tag to the hero section title

    Added a small version tag "v2.0.0-beta" to the hero section title for better visibility and user awareness of the current version.

commit e80de3576c
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 25 01:11:45 2025 -0300

    build(docs): disable eslint and typescript checks during builds

    To streamline the build process and avoid unnecessary interruptions, eslint and typescript checks are now ignored during builds

commit 17dd85241c
Merge: da64d65 f7124ec
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 25 01:01:28 2025 -0300

    Upgrade to v2.0.0-beta (#16)

commit f7124ec346
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 25 00:56:04 2025 -0300

    chore: update environment and docker configurations

    Update .env.example to include SERVER_IP, add metadata to docs layout, and switch docker-compose image tags to 'latest' for consistency and clarity.

commit b443fcb010
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Thu Apr 24 23:31:40 2025 -0300

    docs: update image URLs in README.md

    Update the image URLs in the README.md file to use the new Cloudinary links for better reliability and consistency

commit d106b5346f
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Thu Apr 24 23:28:30 2025 -0300

    docs: update documentation links and README content

    Update all documentation links from 'palmr-docs.kyantech.com.br' to 'palmr.kyantech.com.br' to reflect the new URL. Additionally, update the README.md to include the latest frontend technology stack and correct the screenshot image link. Also, add a hyperlink to the core maintainer's GitHub profile.

commit 7a6df51308
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Thu Apr 24 23:14:25 2025 -0300

    refactor(home): update layout, image URLs, and documentation links

    - Remove unnecessary spaces in JSX elements for cleaner code
    - Add images configuration to next.config.mjs to support remote image sources
    - Replace old image URLs with new Cloudinary-hosted images
    - Update documentation links to include an icon for better UX

commit e40254fea6
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Thu Apr 24 20:17:04 2025 -0300

    feat(home-page): redesign home page with interactive components

    Added new interactive components like animated grids, pulsating buttons, and ripple effects to enhance the user experience. Updated the layout to include sections for features, architecture, and file sharing. Improved the overall design with modern animations and typography.

commit a3ed862ed9
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Thu Apr 24 12:12:11 2025 -0300

    docs: add new documentation pages for Palmr 2.0.0-beta

    This commit introduces several new documentation pages for the Palmr 2.0.0-beta release. The added pages cover topics such as SMTP configuration, available languages, GitHub sponsorship, starring the repository, opening issues, generating shares, and contributing to the project. These additions aim to provide comprehensive guidance for users and contributors, enhancing the overall user experience and supporting the project's growth.

commit 7904333b15
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Wed Apr 23 16:59:06 2025 -0300

    docs: add API documentation and update meta.json for 1.1.7-beta and 2.0.0-beta

    This commit introduces API documentation files for both 1.1.7-beta and 2.0.0-beta versions, including detailed guides on accessing and using Scalar and Swagger-based API documentation. Additionally, it updates the meta.json files for both versions to include the new API section and other relevant entries. The global.css file has also been updated to improve styling for code blocks and headings.

commit 57af56ff7e
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Tue Apr 22 17:46:31 2025 -0300

    feat(docs): update and enhance documentation for v2.0.0-beta

    Add new architecture, installation, and GitHub architecture documentation. Include new banner and architecture images. Refactor meta.json and index.mdx for better structure and clarity. Add sponsor and footer components to the docs layout. Update docker-compose.yaml with new environment variables and port configurations. Remove outdated files and streamline content for v2.0.0-beta release.

commit a2a5b6a88b
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Thu Apr 17 11:49:32 2025 -0300

    refactor: replace Image component with img tag for app logo

    The commit replaces the Next.js `Image` component with the standard HTML `img` tag in the app logo rendering. This change simplifies the implementation and removes unnecessary dependencies. Additionally, the `env` import was removed from the seed file, the docker-compose image tag was updated to a specific version, and remote image patterns were added to the Next.js config.

commit fccc9d559f
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Thu Apr 17 01:16:38 2025 -0300

    chore: remove docker-compose-local.yaml file

    The file was deleted as it is no longer needed for the local development setup. This change simplifies the project structure and reduces maintenance overhead.

commit c1fc52c302
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Thu Apr 17 01:15:19 2025 -0300

    feat(docs): migrate documentation to Next.js with Fumadocs

    This commit migrates the documentation site from Astro to Next.js, leveraging Fumadocs for enhanced functionality and maintainability. The migration includes:
    - New Next.js configuration and setup
    - Integration of Fumadocs for documentation rendering
    - Addition of new documentation assets and images
    - Removal of Astro-related files and configurations

    The migration aims to improve the documentation site's performance, scalability, and developer experience.

commit dca252827c
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Wed Apr 16 15:24:20 2025 -0300

    refactor: remove deprecated Makefile and generate-docker-compose.sh

    The Makefile and generate-docker-compose.sh script were removed as they are no longer needed. The docker-compose.yaml file was updated to include inline comments and use the latest image tag for the palmr-app service. This cleanup simplifies the project structure and ensures clarity in the docker-compose configuration.

commit 32bc17c373
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Wed Apr 16 14:36:16 2025 -0300

    chore: update .gitignore and docker-compose.yaml for better configuration

    - Add `.env` to .gitignore to ignore environment variables file
    - Update docker-compose.yaml to use environment variables for configuration
      and improve comments for clarity

commit efba0b75dc
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Wed Apr 16 13:53:51 2025 -0300

    refactor(web): restructure and clean up project files

    Restructured the project by moving and deleting unused files, updating configurations, and organizing code for better maintainability. This includes removing deprecated models, updating environment settings, and consolidating utility functions.

commit a119ab4d46
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Wed Apr 16 13:40:54 2025 -0300

    build(web): add Dockerfile and docker-compose.yml for production deployment

    Refactor image handling in components to use Next.js Image component
    Remove unused imports and disable ESLint during builds
    Add error logging for i18n and login functionality
    Update Next.js config for standalone output and build optimizations

commit d25f493c7a
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Wed Apr 16 11:17:48 2025 -0300

    feat(i18n): add theme translation support for multiple languages

    Add theme-related translations (toggle, light, dark, system) to JSON files for all supported languages. Update the mode-toggle component to use these translations for theme switching. Also, refactor API route files to improve code consistency and readability.

commit 78e2d05d6c
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Wed Apr 16 10:52:20 2025 -0300

    refactor(api): migrate API endpoints to use proxy routes and update axios instance

    This commit introduces proxy routes for all API endpoints and updates the axios instance to use the new proxy routes. The changes ensure that all API requests are routed through the Next.js API routes, improving security and consistency. Additionally, the axios instance is renamed to `apiInstance` to reflect its purpose more accurately. The metadata generation in layout files is also simplified by removing unnecessary API calls.

commit 564ff43843
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Tue Apr 15 10:21:30 2025 -0300

    chore: remove unused SVG files and refactor login types

    Clean up the codebase by deleting unused SVG assets and improve code readability by adding proper spacing and type definitions in the login module

commit 5aea36fd98
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Mon Apr 14 16:55:36 2025 -0300

    feat: add forgot and reset password functionality

    Implement forgot and reset password features, including form components, hooks for form handling, and layout/page components. This allows users to request a password reset and set a new password securely.

commit 7d6e484c2b
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Mon Apr 14 15:55:15 2025 -0300

    feat(shares): add share management and public share components

    This commit introduces new components and hooks for managing shares and viewing public shares. It includes the following key changes:
    - Added components for share management, such as `SharesHeader`, `SharesSearch`, and `SharesTableContainer`.
    - Implemented `useShares` hook to handle share data fetching and state management.
    - Created `PublicSharePage` and related components for viewing public shares, including `ShareDetails`, `PasswordModal`, and `ShareNotFound`.
    - Added `usePublicShare` hook to manage public share data and password handling.
    - Updated layout and types to support the new features.

commit e50abf953e
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Mon Apr 14 15:21:31 2025 -0300

    feat(shares): add shares page with search, table, and modals

    Introduce a new shares page that includes a search bar, shares table, and various modals for managing shares. The page supports creating, editing, deleting, and generating links for shares. Additionally, it includes functionality for notifying recipients and viewing share details. The layout and components are designed to be reusable and maintainable.

commit 15dce5dab1
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Mon Apr 14 11:53:43 2025 -0300

    feat(layout): add app name to page titles and implement favicon component

    Add app name to page titles by fetching it from the configs endpoint. Introduce a new `Favicon` component to handle dynamic favicon rendering based on the app logo. Remove manual favicon updates from the app info context as it is now handled by the `Favicon` component.

commit 20fee6b449
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Mon Apr 14 10:37:00 2025 -0300

    refactor(users-management): reorganize imports and clean up code for better readability

    This commit focuses on improving the readability and maintainability of the users-management module by reorganizing imports, removing unnecessary whitespace, and standardizing code formatting. No functional changes were made.

commit e03ca7e0dc
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Mon Apr 14 10:30:45 2025 -0300

    feat(users-management): add users management module

    Introduce a new users management module that includes features for creating, reading, updating, and deleting users. The module includes components for user tables, modals for user actions, and hooks for managing user state and interactions. This replaces the previous admin page with a more comprehensive and modular approach to user management.

commit ab6c634782
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 11 16:45:03 2025 -0300

    feat(profile): add profile page with form, password, and image components

    Introduce a new profile page that includes forms for updating user profile information, changing passwords, and managing profile pictures. The page is protected and integrates with the existing authentication system. Additionally, update validation messages for better clarity and consistency.

commit 6a933891c8
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 11 16:43:51 2025 -0300

    refactor(settings): reorganize imports and improve code readability

    Restructured imports across multiple files to follow a consistent order and improve readability. Also, adjusted some code formatting for better maintainability.

commit 5cd7acc158
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 11 15:47:42 2025 -0300

    feat(settings): add settings page with layout, form, and components

    Introduce a new settings page with a structured layout, form components, and hooks for managing application settings. This includes the addition of settings-specific types, constants, and UI components such as `SettingsForm`, `SettingsGroup`, and `SettingsInput`. The `useSettings` hook handles configuration loading and updates, while the `LogoInput` component manages logo uploads and removal. The `Select` component from Radix UI is also added to support dropdown functionality.

commit b4cecf9e32
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 11 15:01:32 2025 -0300

    style: reorganize imports and format code for consistency

    Refactor import statements and code formatting across multiple files to improve readability and maintain consistency. This includes reordering imports, fixing linting issues, and standardizing code style.

commit e55f090235
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 11 14:59:52 2025 -0300

    feat(files): add file management components and hooks

    Introduce new components and hooks for managing files, including file list, search bar, empty state, and modals. This includes the addition of a breadcrumb component for better navigation and the use of client-side rendering for specific components. The changes aim to improve the user experience and maintainability of the file management system.

commit 0a738430e7
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 11 14:18:11 2025 -0300

    feat: add new utility functions and UI components for file management

    This commit introduces several new utility functions and UI components to enhance file management capabilities. Key additions include:
    - `generateSafeFileName` utility for creating safe file names.
    - `customNanoid` utility for generating custom IDs.
    - New UI components: `AspectRatio`, `Loader`, `Switch`, `ScrollArea`, and various modals for file actions, share management, and file preview.
    - Updated translations and package dependencies to support new features.

commit 6c7117cc14
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Wed Apr 9 15:57:09 2025 -0300

    style: format code and fix linting issues across multiple files

    Refactor code to improve readability and consistency by applying Prettier formatting rules. This includes fixing trailing commas, sorting imports, and ensuring consistent code style. No functional changes were made.

commit 61cf88b41f
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Wed Apr 9 15:48:00 2025 -0300

    feat(dashboard): add dashboard components and utilities

    This commit introduces new components and utilities for the dashboard, including storage usage, quick access cards, recent files, and recent shares. It also adds file and share management hooks, along with new UI components like progress bars, separators, and avatars. The changes enhance the dashboard's functionality and improve user experience by providing quick access to essential features and better visual feedback.

    The commit includes:
    - New components for storage usage, quick access, recent files, and shares
    - File and share management hooks for CRUD operations
    - Utility functions for file size formatting and file icons
    - New UI components like progress bars, separators, and avatars
    - Updated translations and styles for consistency

commit b077154c22
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Tue Apr 8 15:06:17 2025 -0300

    feat(auth): add protected routes and enhance auth context

    Implement protected routes for admin and dashboard pages to restrict access based on authentication and admin status. Enhance the auth context to handle user data validation and improve error handling during authentication checks.

commit 9e35d27497
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Mon Apr 7 16:14:18 2025 -0300

    feat: add initial project setup with config, models, and assets

    This commit introduces the initial project setup including configuration files, API models, and necessary assets. The changes include:
    - Added Prettier and PostCSS configuration files
    - Included favicon and public assets like SVGs
    - Set up Next.js and theme provider configurations
    - Added TypeScript models for API endpoints and responses

commit da64d65401
Merge: ca7bdef 7b2f15d
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 4 23:40:35 2025 -0300

    feat: enable reverse proxy support and add pnpm.lock for custom builds in apps/web (#13)

commit 7b2f15dcd5
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 4 23:37:08 2025 -0300

    refactor: remove lock files from .gitignore and update vite config

    Remove unnecessary lock files from .gitignore to streamline version control. Update Vite configuration to allow all hosts in both development and preview modes for better accessibility

commit ca7bdefcdb
Merge: 92b437e 1768aa8
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 4 00:44:02 2025 -0300

    Merge branch 'main' of github.com:kyantech/Palmr

commit 92b437ee36
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 4 00:41:55 2025 -0300

    chore: bump version to 1.1.6 across all apps

commit fcaef88850
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 4 00:40:49 2025 -0300

    docs: update installation guide with security and deployment details

    Add a new section "Quick Start with Default Docker Compose" to emphasize the risks of using default credentials and provide recommendations for secure deployment. Clarify the usage of Docker Compose for different environments (local, production) and update port configuration recommendations with a warning about ReactJS limitations.

commit 68d6fd09af
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Fri Apr 4 00:12:02 2025 -0300

    chore: add docker-compose.yaml and update .gitignore

    Add docker-compose.yaml to define services for the application stack, including API, app, MinIO, and PostgreSQL. Remove docker-compose.yaml from .gitignore to track it in version control

commit 1768aa81b7
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Thu Apr 3 16:16:27 2025 -0300

    Update README.md

commit 644fc7aa30
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Thu Apr 3 16:15:34 2025 -0300

    Update README.md

commit cc6fe6d62e
Author: Daniel Luiz Alves <daniel.xcoders@gmail.com>
Date:   Thu Apr 3 16:15:06 2025 -0300

    Update README.md
2025-04-27 23:23:49 -03:00
Daniel Luiz Alves
359d0a0403 docs(installation): add command to generate .env file
Add a command to generate a .env file with the `server_ip` configuration to simplify the setup process for users. This command can be executed in the server terminal at the same path as the docker-compose.yaml file.
2025-04-25 14:11:09 -03:00
Daniel Luiz Alves
c3967eca72 refactor(web): remove unused config fetch in layout metadata
Simplify the metadata generation in the layout component by removing the unnecessary fetch of app configurations. This reduces complexity and improves maintainability.
2025-04-25 13:42:26 -03:00
54 changed files with 851 additions and 215 deletions

View File

@@ -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:
![Landing Page](/assets/v1/general/lp.png)
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:
![Login Page](/assets/v1/ui/login.png)
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:
![Dashboard](/assets/v1/ui/dashboard.png)
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:
![Menu](/assets/v1/ui/menu.png)
1. From the available options in the dropdown menu, select **"Profile"**. This selection will navigate you to the comprehensive profile settings interface:
![Profile](/assets/v1/ui/profile.png)
---
### 📝 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.
![Profile Picture](/assets/v1/ui/profile_picture.png)
> 💡 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.

View File

@@ -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.*

View File

@@ -12,7 +12,6 @@
"manual-installation",
"api",
"---How to use Palmr.---",
"first-login",
"manage-users",
"uploading-files",
"generate-share",

View File

@@ -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,

View File

@@ -10,4 +10,4 @@ MINIO_REGION="sa-east-1"
MINIO_BUCKET_NAME="files"
PORT=3333
SERVER_IP="localhost"
MAX_FILESIZE="1073741824"

View File

@@ -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) {

View File

@@ -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();
});
});

View File

@@ -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);

View File

@@ -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") }),
},

View File

@@ -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",
};
}

View File

@@ -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 {

View File

@@ -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"),

View File

@@ -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",

View File

@@ -23,6 +23,7 @@ export const createRegisterUserSchema = async () => {
export type RegisterUserInput = BaseRegisterUserInput & {
password: string;
isAdmin?: boolean;
};
export const UpdateUserSchema = z.object({

View File

@@ -24,6 +24,7 @@ export class PrismaUserRepository implements IUserRepository {
email: data.email,
password: data.password,
image: data.image,
isAdmin: data.isAdmin,
},
});
}

View File

@@ -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);

View File

@@ -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
View File

@@ -0,0 +1 @@
API_BASE_URL=http:localhost:3333

2
apps/web/.gitignore vendored
View File

@@ -31,7 +31,7 @@ yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
.env
# vercel
.vercel

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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"
}
}
}

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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ıı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": {

View File

@@ -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": {

View File

@@ -12,15 +12,15 @@ const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '**',
protocol: "https",
hostname: "**",
},
{
protocol: 'http',
hostname: '**',
protocol: "http",
hostname: "**",
},
],
}
},
};
const withNextIntl = createNextIntlPlugin();

View File

@@ -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")}`,
};
}

View File

@@ -24,7 +24,5 @@ export async function GET(req: NextRequest) {
res.headers.set("Set-Cookie", setCookie.join(","));
}
console.log(res);
return res;
}

View File

@@ -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");

View 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;
}

View File

@@ -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;
}
}

View File

@@ -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">

View File

@@ -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>
);
}

View 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>
);
}

View File

@@ -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>

View File

@@ -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"

View File

@@ -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}`, ""),

View File

@@ -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 });
}
},
};

View File

@@ -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

View File

@@ -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 };

View 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;
};

View 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;
};

View 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;
};

View File

@@ -13,4 +13,6 @@ export type GetAppInfo200 = {
appDescription: string;
/** The application logo */
appLogo: string;
/** The application first Access */
firstUserAccess: boolean;
};

View File

@@ -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,

View 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;

View File

@@ -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