mirror of
				https://github.com/kyantech/Palmr.git
				synced 2025-11-04 05:53:23 +00:00 
			
		
		
		
	Compare commits
	
		
			21 Commits
		
	
	
		
			v3.0.0-bet
			...
			v3.0.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					6e526f7f88 | ||
| 
						 | 
					858852c8cd | ||
| 
						 | 
					363dedbb2c | ||
| 
						 | 
					cd215c79b8 | ||
| 
						 | 
					98586efbcd | ||
| 
						 | 
					c724e644c7 | ||
| 
						 | 
					555ff18a87 | ||
| 
						 | 
					5100e1591b | ||
| 
						 | 
					6de29bbf07 | ||
| 
						 | 
					39c47be940 | ||
| 
						 | 
					76d96816bc | ||
| 
						 | 
					b3e7658a76 | ||
| 
						 | 
					61a579aeb3 | ||
| 
						 | 
					cc9c375774 | ||
| 
						 | 
					016006ba3d | ||
| 
						 | 
					cbc567c6a8 | ||
| 
						 | 
					25b4d886f7 | ||
| 
						 | 
					98953e042b | ||
| 
						 | 
					9e06a67593 | ||
| 
						 | 
					9682f96905 | ||
| 
						 | 
					d2c69c3b36 | 
@@ -132,6 +132,7 @@ set -e
 | 
			
		||||
 | 
			
		||||
echo "Starting Palmr Application..."
 | 
			
		||||
echo "Storage Mode: \${ENABLE_S3:-false}"
 | 
			
		||||
echo "Secure Site: \${SECURE_SITE:-false}"
 | 
			
		||||
echo "Database: SQLite"
 | 
			
		||||
 | 
			
		||||
# Set global environment variables
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@
 | 
			
		||||
    "configuring-smtp",
 | 
			
		||||
    "available-languages",
 | 
			
		||||
    "uid-gid-configuration",
 | 
			
		||||
    "reverse-proxy-configuration",
 | 
			
		||||
    "password-reset-without-smtp",
 | 
			
		||||
    "oidc-authentication",
 | 
			
		||||
    "---Developers---",
 | 
			
		||||
 
 | 
			
		||||
@@ -56,6 +56,7 @@ services:
 | 
			
		||||
    environment:
 | 
			
		||||
      - ENABLE_S3=false
 | 
			
		||||
      - ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY
 | 
			
		||||
      # - SECURE_SITE=false # Set to true if you are using a reverse proxy
 | 
			
		||||
    ports:
 | 
			
		||||
      - "5487:5487" # Web interface
 | 
			
		||||
      - "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
 | 
			
		||||
@@ -91,6 +92,7 @@ services:
 | 
			
		||||
    environment:
 | 
			
		||||
      - ENABLE_S3=false
 | 
			
		||||
      - ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY
 | 
			
		||||
      # - SECURE_SITE=false # Set to true if you are using a reverse proxy
 | 
			
		||||
      # Optional: Set custom UID/GID for file permissions
 | 
			
		||||
      # - PALMR_UID=1000
 | 
			
		||||
      # - PALMR_GID=1000
 | 
			
		||||
@@ -121,9 +123,12 @@ Configure Palmr. behavior through environment variables:
 | 
			
		||||
| ---------------- | ------- | ------------------------------------------------------- |
 | 
			
		||||
| `ENABLE_S3`      | `false` | Enable S3-compatible storage                            |
 | 
			
		||||
| `ENCRYPTION_KEY` | -       | **Required**: Minimum 32 characters for file encryption |
 | 
			
		||||
| `SECURE_SITE`    | `false` | Enable secure cookies for HTTPS/reverse proxy setups    |
 | 
			
		||||
 | 
			
		||||
> **⚠️ Security Warning**: Always change the `ENCRYPTION_KEY` in production. This key encrypts your files - losing it makes files permanently inaccessible.
 | 
			
		||||
 | 
			
		||||
> **🔗 Reverse Proxy**: If deploying behind a reverse proxy (Traefik, Nginx, etc.), set `SECURE_SITE=true` and review our [Reverse Proxy Configuration](/docs/3.0-beta/reverse-proxy-configuration) guide for proper setup.
 | 
			
		||||
 | 
			
		||||
### Generate Secure Encryption Keys
 | 
			
		||||
 | 
			
		||||
Need a strong key for `ENCRYPTION_KEY`? Use our built-in generator to create cryptographically secure keys:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										199
									
								
								apps/docs/content/docs/3.0-beta/reverse-proxy-configuration.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								apps/docs/content/docs/3.0-beta/reverse-proxy-configuration.mdx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,199 @@
 | 
			
		||||
---
 | 
			
		||||
title: Reverse Proxy Configuration
 | 
			
		||||
icon: "Shield"
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
When deploying **Palmr.** behind a reverse proxy (like Traefik, Nginx, or Cloudflare), you need to configure secure cookie settings to ensure proper authentication. This guide covers the `SECURE_SITE` environment variable and related proxy configurations.
 | 
			
		||||
 | 
			
		||||
## Overview
 | 
			
		||||
 | 
			
		||||
Reverse proxies terminate SSL/TLS connections and forward requests to Palmr., which can cause authentication issues if cookies aren't configured properly for HTTPS environments. The `SECURE_SITE` environment variable controls cookie security settings to handle these scenarios.
 | 
			
		||||
 | 
			
		||||
## The SECURE_SITE Environment Variable
 | 
			
		||||
 | 
			
		||||
The `SECURE_SITE` variable configures how Palmr. handles authentication cookies based on your deployment environment:
 | 
			
		||||
 | 
			
		||||
### Configuration Options
 | 
			
		||||
 | 
			
		||||
| Value   | Cookie Settings                       | Use Case                            |
 | 
			
		||||
| ------- | ------------------------------------- | ----------------------------------- |
 | 
			
		||||
| `true`  | `secure: true`, `sameSite: "lax"`     | HTTPS/Production with reverse proxy |
 | 
			
		||||
| `false` | `secure: false`, `sameSite: "strict"` | HTTP/Development (default)          |
 | 
			
		||||
 | 
			
		||||
### When to Use SECURE_SITE=true
 | 
			
		||||
 | 
			
		||||
Set `SECURE_SITE=true` in the following scenarios:
 | 
			
		||||
 | 
			
		||||
- ✅ **Reverse Proxy with HTTPS**: Traefik, Nginx, HAProxy with SSL termination
 | 
			
		||||
- ✅ **Cloud Providers**: Cloudflare, AWS ALB, Azure Application Gateway
 | 
			
		||||
- ✅ **CDN with HTTPS**: Any CDN that terminates SSL
 | 
			
		||||
- ✅ **Production Deployments**: When users access via HTTPS
 | 
			
		||||
 | 
			
		||||
### When to Use SECURE_SITE=false
 | 
			
		||||
 | 
			
		||||
Keep `SECURE_SITE=false` (default) for:
 | 
			
		||||
 | 
			
		||||
- ✅ **Local Development**: Running on `http://localhost`
 | 
			
		||||
- ✅ **Direct HTTP Access**: No reverse proxy involved
 | 
			
		||||
- ✅ **Testing Environments**: When using HTTP
 | 
			
		||||
- ✅ **HTTP Reverse Proxy**: Nginx, Apache, etc. without SSL termination
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## HTTP Reverse Proxy Setup
 | 
			
		||||
 | 
			
		||||
**Docker Compose for HTTP Nginx:**
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
environment:
 | 
			
		||||
  - SECURE_SITE=false # HTTP = false
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
> **⚠️ HTTP Security**: Remember that HTTP transmits data in plain text. Consider using HTTPS in production environments.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Troubleshooting Authentication Issues
 | 
			
		||||
 | 
			
		||||
### Common Symptoms
 | 
			
		||||
 | 
			
		||||
If you experience authentication issues behind a reverse proxy:
 | 
			
		||||
 | 
			
		||||
- ❌ Login appears successful but redirects to login page
 | 
			
		||||
- ❌ "No Authorization was found in request.cookies" errors
 | 
			
		||||
- ❌ API requests return 401 Unauthorized
 | 
			
		||||
- ❌ User registration fails silently
 | 
			
		||||
 | 
			
		||||
### Diagnostic Steps
 | 
			
		||||
 | 
			
		||||
1. **Check Browser Developer Tools**:
 | 
			
		||||
 | 
			
		||||
   - Look for cookies in Application/Storage tab
 | 
			
		||||
   - Verify cookie has `Secure` flag when using HTTPS
 | 
			
		||||
   - Check if `SameSite` attribute is appropriate
 | 
			
		||||
 | 
			
		||||
2. **Verify Environment Variables**:
 | 
			
		||||
 | 
			
		||||
   ```bash
 | 
			
		||||
   docker exec -it palmr env | grep SECURE_SITE
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
3. **Test Cookie Settings**:
 | 
			
		||||
   - With `SECURE_SITE=false`: Should work on HTTP
 | 
			
		||||
   - With `SECURE_SITE=true`: Should work on HTTPS
 | 
			
		||||
 | 
			
		||||
### Common Fixes
 | 
			
		||||
 | 
			
		||||
**Problem**: Authentication fails with reverse proxy
 | 
			
		||||
 | 
			
		||||
**Solution**: Set `SECURE_SITE=true` and ensure proper headers:
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
environment:
 | 
			
		||||
  - SECURE_SITE=true
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Problem**: Mixed content errors
 | 
			
		||||
 | 
			
		||||
**Solution**: Ensure proxy passes correct headers:
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
# Traefik
 | 
			
		||||
- "traefik.http.middlewares.palmr-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
 | 
			
		||||
 | 
			
		||||
# Nginx
 | 
			
		||||
proxy_set_header X-Forwarded-Proto $scheme;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Problem**: Authentication fails with HTTP reverse proxy
 | 
			
		||||
 | 
			
		||||
**Solution**: Use `SECURE_SITE=false` and ensure proper cookie headers:
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
environment:
 | 
			
		||||
  - SECURE_SITE=false # For HTTP proxy
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```nginx
 | 
			
		||||
# Nginx - Add these headers for cookie handling
 | 
			
		||||
proxy_set_header Cookie $http_cookie;
 | 
			
		||||
proxy_pass_header Set-Cookie;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Problem**: SQLite "readonly database" error with bind mounts
 | 
			
		||||
 | 
			
		||||
**Solution**: Configure proper UID/GID permissions:
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
environment:
 | 
			
		||||
  - PALMR_UID=1000 # Your host UID (check with: id)
 | 
			
		||||
  - PALMR_GID=1000 # Your host GID
 | 
			
		||||
  - ENCRYPTION_KEY=your-key-here
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
> **💡 Note**: Check your host UID/GID with `id` command and use those values. See [UID/GID Configuration](/docs/3.0-beta/uid-gid-configuration) for detailed setup.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Security Considerations
 | 
			
		||||
 | 
			
		||||
> **⚠️ Important**: Always use HTTPS in production environments. The `SECURE_SITE=true` setting ensures cookies are only sent over encrypted connections.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Advanced Configuration
 | 
			
		||||
 | 
			
		||||
### Multiple Domains
 | 
			
		||||
 | 
			
		||||
If serving Palmr. on multiple domains, ensure consistent cookie settings:
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
environment:
 | 
			
		||||
  - SECURE_SITE=true # Use for all HTTPS domains
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Development vs Production
 | 
			
		||||
 | 
			
		||||
Use environment-specific configurations:
 | 
			
		||||
 | 
			
		||||
**Development (HTTP):**
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
environment:
 | 
			
		||||
  - SECURE_SITE=false
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Production (HTTPS):**
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
environment:
 | 
			
		||||
  - SECURE_SITE=true
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Health Checks
 | 
			
		||||
 | 
			
		||||
Add health checks to ensure proper proxy configuration:
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
services:
 | 
			
		||||
  palmr:
 | 
			
		||||
    # ... other config
 | 
			
		||||
    healthcheck:
 | 
			
		||||
      test: ["CMD", "curl", "-f", "http://localhost:5487/api/health"]
 | 
			
		||||
      interval: 30s
 | 
			
		||||
      timeout: 10s
 | 
			
		||||
      retries: 3
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Need Help?
 | 
			
		||||
 | 
			
		||||
If you're still experiencing issues after following this guide:
 | 
			
		||||
 | 
			
		||||
1. **Check the Logs**: `docker logs palmr`
 | 
			
		||||
2. **Verify Headers**: Use browser dev tools or `curl -I`
 | 
			
		||||
3. **Test Direct Access**: Try accessing Palmr. directly (bypassing proxy)
 | 
			
		||||
4. **Open an Issue**: [Report bugs on GitHub](https://github.com/kyantech/Palmr/issues)
 | 
			
		||||
 | 
			
		||||
> **💡 Pro Tip**: When reporting issues, include your reverse proxy configuration and any relevant error messages from both Palmr. and your proxy logs.
 | 
			
		||||
@@ -11,6 +11,7 @@ const envSchema = z.object({
 | 
			
		||||
  S3_REGION: z.string().optional(),
 | 
			
		||||
  S3_BUCKET_NAME: z.string().optional(),
 | 
			
		||||
  S3_FORCE_PATH_STYLE: z.union([z.literal("true"), z.literal("false")]).default("false"),
 | 
			
		||||
  SECURE_SITE: z.union([z.literal("true"), z.literal("false")]).default("false"),
 | 
			
		||||
  DATABASE_URL: z.string().optional().default("file:/app/server/prisma/palmr.db"),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,21 @@ import { FastifyReply, FastifyRequest } from "fastify";
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
import path from "path";
 | 
			
		||||
 | 
			
		||||
const uploadsDir = path.join(process.cwd(), "uploads/logo");
 | 
			
		||||
const isDocker = (() => {
 | 
			
		||||
  try {
 | 
			
		||||
    require("fs").statSync("/.dockerenv");
 | 
			
		||||
    return true;
 | 
			
		||||
  } catch {
 | 
			
		||||
    try {
 | 
			
		||||
      return require("fs").readFileSync("/proc/self/cgroup", "utf8").includes("docker");
 | 
			
		||||
    } catch {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
const baseDir = isDocker ? "/app/server" : process.cwd();
 | 
			
		||||
const uploadsDir = path.join(baseDir, "uploads/logo");
 | 
			
		||||
if (!fs.existsSync(uploadsDir)) {
 | 
			
		||||
  fs.mkdirSync(uploadsDir, { recursive: true });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import { env } from "../../env";
 | 
			
		||||
import { LoginSchema, RequestPasswordResetSchema, createResetPasswordSchema } from "./dto";
 | 
			
		||||
import { AuthService } from "./service";
 | 
			
		||||
import { FastifyReply, FastifyRequest } from "fastify";
 | 
			
		||||
@@ -17,8 +18,8 @@ export class AuthController {
 | 
			
		||||
      reply.setCookie("token", token, {
 | 
			
		||||
        httpOnly: true,
 | 
			
		||||
        path: "/",
 | 
			
		||||
        secure: false,
 | 
			
		||||
        sameSite: "strict",
 | 
			
		||||
        secure: env.SECURE_SITE === "true" ? true : false,
 | 
			
		||||
        sameSite: env.SECURE_SITE === "true" ? "lax" : "strict",
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return reply.send({ user });
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,35 @@ import { ConfigService } from "../config/service";
 | 
			
		||||
import { PrismaClient } from "@prisma/client";
 | 
			
		||||
import { exec } from "child_process";
 | 
			
		||||
import { promisify } from "util";
 | 
			
		||||
import fs from 'node:fs';
 | 
			
		||||
 | 
			
		||||
const execAsync = promisify(exec);
 | 
			
		||||
const prisma = new PrismaClient();
 | 
			
		||||
 | 
			
		||||
export class StorageService {
 | 
			
		||||
  private configService = new ConfigService();
 | 
			
		||||
  private isDockerCached = undefined;
 | 
			
		||||
 | 
			
		||||
  private _hasDockerEnv() {
 | 
			
		||||
    try {
 | 
			
		||||
      fs.statSync('/.dockerenv');
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _hasDockerCGroup() {
 | 
			
		||||
    try {
 | 
			
		||||
      return fs.readFileSync('/proc/self/cgroup', 'utf8').includes('docker');
 | 
			
		||||
    } catch {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _isDocker() {
 | 
			
		||||
    return this.isDockerCached ?? (this._hasDockerEnv() || this._hasDockerCGroup());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getDiskSpace(
 | 
			
		||||
    userId?: string,
 | 
			
		||||
@@ -20,11 +43,14 @@ export class StorageService {
 | 
			
		||||
  }> {
 | 
			
		||||
    try {
 | 
			
		||||
      if (isAdmin) {
 | 
			
		||||
        const isDocker = this._isDocker();
 | 
			
		||||
        const pathToCheck = isDocker ? "/app/server/uploads" : ".";
 | 
			
		||||
 | 
			
		||||
        const command = process.platform === "win32"
 | 
			
		||||
          ? "wmic logicaldisk get size,freespace,caption"
 | 
			
		||||
          : process.platform === "darwin"
 | 
			
		||||
            ? "df -k ." 
 | 
			
		||||
            : "df -B1 .";
 | 
			
		||||
            ? `df -k ${pathToCheck}`
 | 
			
		||||
            : `df -B1 ${pathToCheck}`;
 | 
			
		||||
 | 
			
		||||
        const { stdout } = await execAsync(command);
 | 
			
		||||
        let total = 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -14,20 +14,26 @@ export async function userRoutes(app: FastifyInstance) {
 | 
			
		||||
      const usersCount = await prisma.user.count();
 | 
			
		||||
 | 
			
		||||
      if (usersCount > 0) {
 | 
			
		||||
        await request.jwtVerify();
 | 
			
		||||
        if (!request.user.isAdmin) {
 | 
			
		||||
        try {
 | 
			
		||||
          await request.jwtVerify();
 | 
			
		||||
          if (!request.user.isAdmin) {
 | 
			
		||||
            return reply
 | 
			
		||||
              .status(403)
 | 
			
		||||
              .send({ error: "Access restricted to administrators" })
 | 
			
		||||
              .description("Access restricted to administrators");
 | 
			
		||||
          }
 | 
			
		||||
        } catch (authErr) {
 | 
			
		||||
          console.error(authErr);
 | 
			
		||||
          return reply
 | 
			
		||||
            .status(403)
 | 
			
		||||
            .send({ error: "Access restricted to administrators" })
 | 
			
		||||
            .description("Access restricted to administrators");
 | 
			
		||||
            .status(401)
 | 
			
		||||
            .send({ error: "Unauthorized: a valid token is required to access this resource." })
 | 
			
		||||
            .description("Unauthorized: a valid token is required to access this resource.");
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      // If usersCount is 0, allow the request to proceed without authentication
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.error(err);
 | 
			
		||||
      return reply
 | 
			
		||||
        .status(401)
 | 
			
		||||
        .send({ error: "Unauthorized: a valid token is required to access this resource." })
 | 
			
		||||
        .description("Unauthorized: a valid token is required to access this resource.");
 | 
			
		||||
      return reply.status(500).send({ error: "Internal server error" }).description("Internal server error");
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,16 +9,31 @@ import { pipeline } from "stream/promises";
 | 
			
		||||
 | 
			
		||||
export class FilesystemStorageProvider implements StorageProvider {
 | 
			
		||||
  private static instance: FilesystemStorageProvider;
 | 
			
		||||
  private uploadsDir = path.join(process.cwd(), "uploads");
 | 
			
		||||
  private uploadsDir: string;
 | 
			
		||||
  private encryptionKey = env.ENCRYPTION_KEY;
 | 
			
		||||
  private uploadTokens = new Map<string, { objectName: string; expiresAt: number }>();
 | 
			
		||||
  private downloadTokens = new Map<string, { objectName: string; expiresAt: number; fileName?: string }>();
 | 
			
		||||
 | 
			
		||||
  private constructor() {
 | 
			
		||||
    this.uploadsDir = this.isDocker() ? "/app/server/uploads" : path.join(process.cwd(), "uploads");
 | 
			
		||||
 | 
			
		||||
    this.ensureUploadsDir();
 | 
			
		||||
    setInterval(() => this.cleanExpiredTokens(), 5 * 60 * 1000);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private isDocker(): boolean {
 | 
			
		||||
    try {
 | 
			
		||||
      fsSync.statSync("/.dockerenv");
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch {
 | 
			
		||||
      try {
 | 
			
		||||
        return fsSync.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
 | 
			
		||||
      } catch {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public static getInstance(): FilesystemStorageProvider {
 | 
			
		||||
    if (!FilesystemStorageProvider.instance) {
 | 
			
		||||
      FilesystemStorageProvider.instance = new FilesystemStorageProvider();
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ import { storageRoutes } from "./modules/storage/routes";
 | 
			
		||||
import { userRoutes } from "./modules/user/routes";
 | 
			
		||||
import fastifyMultipart from "@fastify/multipart";
 | 
			
		||||
import fastifyStatic from "@fastify/static";
 | 
			
		||||
import * as fsSync from "fs";
 | 
			
		||||
import * as fs from "fs/promises";
 | 
			
		||||
import crypto from "node:crypto";
 | 
			
		||||
import path from "path";
 | 
			
		||||
@@ -26,21 +27,36 @@ if (typeof global.crypto === "undefined") {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function ensureDirectories() {
 | 
			
		||||
  const uploadsDir = path.join(process.cwd(), "uploads");
 | 
			
		||||
  const tempChunksDir = path.join(process.cwd(), "temp-chunks");
 | 
			
		||||
  // Use /app/server paths in Docker, current directory for local development
 | 
			
		||||
  const isDocker = (() => {
 | 
			
		||||
    try {
 | 
			
		||||
      fsSync.statSync("/.dockerenv");
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch {
 | 
			
		||||
      try {
 | 
			
		||||
        return fsSync.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
 | 
			
		||||
      } catch {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })();
 | 
			
		||||
 | 
			
		||||
  const baseDir = isDocker ? "/app/server" : process.cwd();
 | 
			
		||||
  const uploadsDir = path.join(baseDir, "uploads");
 | 
			
		||||
  const tempChunksDir = path.join(baseDir, "temp-chunks");
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    await fs.access(uploadsDir);
 | 
			
		||||
  } catch {
 | 
			
		||||
    await fs.mkdir(uploadsDir, { recursive: true });
 | 
			
		||||
    console.log("📁 Created uploads directory");
 | 
			
		||||
    console.log(`📁 Created uploads directory: ${uploadsDir}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    await fs.access(tempChunksDir);
 | 
			
		||||
  } catch {
 | 
			
		||||
    await fs.mkdir(tempChunksDir, { recursive: true });
 | 
			
		||||
    console.log("📁 Created temp-chunks directory");
 | 
			
		||||
    console.log(`📁 Created temp-chunks directory: ${tempChunksDir}`);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -62,8 +78,24 @@ async function startServer() {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (env.ENABLE_S3 !== "true") {
 | 
			
		||||
    const isDocker = (() => {
 | 
			
		||||
      try {
 | 
			
		||||
        fsSync.statSync("/.dockerenv");
 | 
			
		||||
        return true;
 | 
			
		||||
      } catch {
 | 
			
		||||
        try {
 | 
			
		||||
          return fsSync.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
 | 
			
		||||
        } catch {
 | 
			
		||||
          return false;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })();
 | 
			
		||||
 | 
			
		||||
    const baseDir = isDocker ? "/app/server" : process.cwd();
 | 
			
		||||
    const uploadsPath = path.join(baseDir, "uploads");
 | 
			
		||||
 | 
			
		||||
    await app.register(fastifyStatic, {
 | 
			
		||||
      root: path.join(process.cwd(), "uploads"),
 | 
			
		||||
      root: uploadsPath,
 | 
			
		||||
      prefix: "/uploads/",
 | 
			
		||||
      decorateReply: false,
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,15 @@ import { NextRequest, NextResponse } from "next/server";
 | 
			
		||||
export async function POST(req: NextRequest, { params }: { params: Promise<{ shareId: string }> }) {
 | 
			
		||||
  const cookieHeader = req.headers.get("cookie");
 | 
			
		||||
  const { shareId } = await params;
 | 
			
		||||
  const body = await req.text();
 | 
			
		||||
 | 
			
		||||
  const apiRes = await fetch(`${process.env.API_BASE_URL}/shares/${shareId}/recipients/notify`, {
 | 
			
		||||
  const apiRes = await fetch(`${process.env.API_BASE_URL}/shares/${shareId}/notify`, {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    headers: {
 | 
			
		||||
      "Content-Type": "application/json",
 | 
			
		||||
      cookie: cookieHeader || "",
 | 
			
		||||
    },
 | 
			
		||||
    body: body,
 | 
			
		||||
    redirect: "manual",
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,8 @@ export default function AuthCallbackPage() {
 | 
			
		||||
    if (token) {
 | 
			
		||||
      Cookies.set("token", token, {
 | 
			
		||||
        path: "/",
 | 
			
		||||
        secure: false,
 | 
			
		||||
        sameSite: "strict",
 | 
			
		||||
        secure: window.location.protocol === "https:",
 | 
			
		||||
        sameSite: window.location.protocol === "https:" ? "lax" : "strict",
 | 
			
		||||
        httpOnly: false,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -49,6 +49,17 @@ export function useLogin() {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const checkAuth = async () => {
 | 
			
		||||
      try {
 | 
			
		||||
        const appInfoResponse = await fetch("/api/app/info");
 | 
			
		||||
        const appInfo = await appInfoResponse.json();
 | 
			
		||||
 | 
			
		||||
        if (appInfo.firstUserAccess) {
 | 
			
		||||
          setUser(null);
 | 
			
		||||
          setIsAdmin(false);
 | 
			
		||||
          setIsAuthenticated(false);
 | 
			
		||||
          setIsInitialized(true);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const userResponse = await getCurrentUser();
 | 
			
		||||
        if (!userResponse?.data?.user) {
 | 
			
		||||
          throw new Error("No user data");
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,7 @@ export function LanguageSwitcher() {
 | 
			
		||||
      maxAge: COOKIE_MAX_AGE,
 | 
			
		||||
      path: "/",
 | 
			
		||||
      sameSite: "lax",
 | 
			
		||||
      secure: false,
 | 
			
		||||
      secure: window.location.protocol === "https:",
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    router.refresh();
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const checkAuth = async () => {
 | 
			
		||||
      try {
 | 
			
		||||
        const appInfoResponse = await fetch("/api/app/info");
 | 
			
		||||
        const appInfo = await appInfoResponse.json();
 | 
			
		||||
 | 
			
		||||
        if (appInfo.firstUserAccess) {
 | 
			
		||||
          setUser(null);
 | 
			
		||||
          setIsAdmin(false);
 | 
			
		||||
          setIsAuthenticated(false);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const response = await getCurrentUser();
 | 
			
		||||
        if (!response?.data?.user) {
 | 
			
		||||
          throw new Error("No user data");
 | 
			
		||||
 
 | 
			
		||||
@@ -36,46 +36,86 @@ echo "💾 Database: $DATABASE_URL"
 | 
			
		||||
echo "📁 Creating data directories..."
 | 
			
		||||
mkdir -p /app/server/prisma /app/server/uploads /app/server/temp-chunks /app/server/uploads/logo
 | 
			
		||||
 | 
			
		||||
# Fix ownership of database directory BEFORE database operations
 | 
			
		||||
if [ "$(id -u)" = "0" ]; then
 | 
			
		||||
    echo "🔐 Ensuring proper ownership before database operations..."
 | 
			
		||||
    chown -R $TARGET_UID:$TARGET_GID /app/server/prisma 2>/dev/null || true
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Check if it's a first run (no database file exists)
 | 
			
		||||
if [ ! -f "/app/server/prisma/palmr.db" ]; then
 | 
			
		||||
    echo "🚀 First run detected - setting up database..."
 | 
			
		||||
    
 | 
			
		||||
    # Create database with proper schema path
 | 
			
		||||
    # Create database with proper schema path - run as target user to avoid permission issues
 | 
			
		||||
    echo "🗄️ Creating database schema..."
 | 
			
		||||
    npx prisma db push --schema=./prisma/schema.prisma --skip-generate
 | 
			
		||||
    if [ "$(id -u)" = "0" ]; then
 | 
			
		||||
        su-exec $TARGET_UID:$TARGET_GID npx prisma db push --schema=./prisma/schema.prisma --skip-generate
 | 
			
		||||
    else
 | 
			
		||||
        npx prisma db push --schema=./prisma/schema.prisma --skip-generate
 | 
			
		||||
    fi
 | 
			
		||||
    
 | 
			
		||||
    # Run seed script from application directory (where node_modules is)
 | 
			
		||||
    # Run seed script from application directory (where node_modules is) - as target user
 | 
			
		||||
    echo "🌱 Seeding database..."
 | 
			
		||||
    node ./prisma/seed.js
 | 
			
		||||
    if [ "$(id -u)" = "0" ]; then
 | 
			
		||||
        su-exec $TARGET_UID:$TARGET_GID node ./prisma/seed.js
 | 
			
		||||
    else
 | 
			
		||||
        node ./prisma/seed.js
 | 
			
		||||
    fi
 | 
			
		||||
    
 | 
			
		||||
    echo "✅ Database setup completed!"
 | 
			
		||||
else
 | 
			
		||||
    echo "♻️ Existing database found"
 | 
			
		||||
    
 | 
			
		||||
    # Always run migrations to ensure schema is up to date
 | 
			
		||||
    # Always run migrations to ensure schema is up to date - as target user
 | 
			
		||||
    echo "🔧 Checking for schema updates..."
 | 
			
		||||
    npx prisma db push --schema=./prisma/schema.prisma --skip-generate
 | 
			
		||||
    if [ "$(id -u)" = "0" ]; then
 | 
			
		||||
        su-exec $TARGET_UID:$TARGET_GID npx prisma db push --schema=./prisma/schema.prisma --skip-generate
 | 
			
		||||
    else
 | 
			
		||||
        npx prisma db push --schema=./prisma/schema.prisma --skip-generate
 | 
			
		||||
    fi
 | 
			
		||||
    
 | 
			
		||||
    # Check if configurations exist
 | 
			
		||||
    # Check if configurations exist - as target user
 | 
			
		||||
    echo "🔍 Verifying database configurations..."
 | 
			
		||||
    CONFIG_COUNT=$(node -e "
 | 
			
		||||
        const { PrismaClient } = require('@prisma/client');
 | 
			
		||||
        const prisma = new PrismaClient();
 | 
			
		||||
        prisma.appConfig.count()
 | 
			
		||||
            .then(count => {
 | 
			
		||||
                console.log(count);
 | 
			
		||||
                process.exit(0);
 | 
			
		||||
            })
 | 
			
		||||
            .catch(() => {
 | 
			
		||||
                console.log(0);
 | 
			
		||||
                process.exit(0);
 | 
			
		||||
            });
 | 
			
		||||
    " 2>/dev/null || echo "0")
 | 
			
		||||
    CONFIG_COUNT=$(
 | 
			
		||||
        if [ "$(id -u)" = "0" ]; then
 | 
			
		||||
            su-exec $TARGET_UID:$TARGET_GID node -e "
 | 
			
		||||
                const { PrismaClient } = require('@prisma/client');
 | 
			
		||||
                const prisma = new PrismaClient();
 | 
			
		||||
                prisma.appConfig.count()
 | 
			
		||||
                    .then(count => {
 | 
			
		||||
                        console.log(count);
 | 
			
		||||
                        process.exit(0);
 | 
			
		||||
                    })
 | 
			
		||||
                    .catch(() => {
 | 
			
		||||
                        console.log(0);
 | 
			
		||||
                        process.exit(0);
 | 
			
		||||
                    });
 | 
			
		||||
            " 2>/dev/null || echo "0"
 | 
			
		||||
        else
 | 
			
		||||
            node -e "
 | 
			
		||||
                const { PrismaClient } = require('@prisma/client');
 | 
			
		||||
                const prisma = new PrismaClient();
 | 
			
		||||
                prisma.appConfig.count()
 | 
			
		||||
                    .then(count => {
 | 
			
		||||
                        console.log(count);
 | 
			
		||||
                        process.exit(0);
 | 
			
		||||
                    })
 | 
			
		||||
                    .catch(() => {
 | 
			
		||||
                        console.log(0);
 | 
			
		||||
                        process.exit(0);
 | 
			
		||||
                    });
 | 
			
		||||
            " 2>/dev/null || echo "0"
 | 
			
		||||
        fi
 | 
			
		||||
    )
 | 
			
		||||
    
 | 
			
		||||
    if [ "$CONFIG_COUNT" -eq "0" ]; then
 | 
			
		||||
        echo "🌱 No configurations found, running seed..."
 | 
			
		||||
        # Always run seed from application directory where node_modules is available
 | 
			
		||||
        node ./prisma/seed.js
 | 
			
		||||
        # Always run seed from application directory where node_modules is available - as target user
 | 
			
		||||
        if [ "$(id -u)" = "0" ]; then
 | 
			
		||||
            su-exec $TARGET_UID:$TARGET_GID node ./prisma/seed.js
 | 
			
		||||
        else
 | 
			
		||||
            node ./prisma/seed.js
 | 
			
		||||
        fi
 | 
			
		||||
    else
 | 
			
		||||
        echo "✅ Found $CONFIG_COUNT configurations"
 | 
			
		||||
    fi
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user