Compare commits

...

17 Commits

Author SHA1 Message Date
Daniel Luiz Alves
98586efbcd v3.0.0-beta.5 (#72) 2025-06-18 18:31:09 -03:00
Daniel Luiz Alves
c724e644c7 fix: update notification endpoint and include request body in API call
- Changed the API endpoint for notifying recipients to include the shareId directly in the URL.
- Added the request body to the fetch call to ensure proper data is sent with the notification request.
- Set the Content-Type header to application/json for the request.
2025-06-18 18:14:20 -03:00
Daniel Luiz Alves
555ff18a87 feat: implement Docker compatibility for file storage paths (#71) 2025-06-18 18:06:51 -03:00
Daniel Luiz Alves
5100e1591b feat: implement Docker compatibility for file storage paths
- Added checks to determine if the application is running in a Docker environment.
- Updated file storage paths to use `/app/server` in Docker and the current working directory for local development.
- Ensured consistent directory creation for uploads and temporary chunks across different environments.
2025-06-18 18:05:46 -03:00
Daniel Luiz Alves
6de29bbf07 fix: standardize environment variable imports and enhance user auth (#69) 2025-06-18 17:08:59 -03:00
Daniel Luiz Alves
39c47be940 fix: standardize environment variable imports and enhance user authentication error handling
- Updated imports for environment variables in auth and email services to ensure consistency.
- Improved error handling in user routes to provide more specific responses for unauthorized access and internal server errors.
2025-06-18 16:57:11 -03:00
Daniel Luiz Alves
76d96816bc v3.0.0-beta.3 (#68) 2025-06-18 16:19:24 -03:00
Daniel Luiz Alves
b3e7658a76 feat: enhance authentication flow and improve database setup script (#67) 2025-06-18 15:32:41 -03:00
Daniel Luiz Alves
61a579aeb3 feat: enhance authentication flow and improve database setup script
- Added a check for first user access in the authentication context to handle initial user setup.
- Updated the server start script to ensure proper ownership and permissions for database operations, enhancing compatibility with Docker environments.
- Refactored database seeding and configuration checks to run as the target user, preventing permission issues during setup.
2025-06-18 15:32:12 -03:00
Daniel Luiz Alves
cc9c375774 feat: add reverse proxy support (#66) 2025-06-18 12:44:39 -03:00
Daniel Luiz Alves
016006ba3d fix: storage calculation when running within docker (#65) 2025-06-18 12:44:20 -03:00
ruohki
cbc567c6a8 fixed logic error 2025-06-18 17:26:23 +02:00
Daniel Luiz Alves
25b4d886f7 docs: Update reverse proxy configuration to address SQLite "readonly database" error
- Added guidance for configuring proper UID/GID permissions to resolve SQLite issues with bind mounts.
- Included a note on checking host UID/GID and linked to detailed setup documentation for clarity.
2025-06-18 12:23:00 -03:00
ruohki
98953e042b check if runs within docker to pick storage loc 2025-06-18 17:16:11 +02:00
Daniel Luiz Alves
9e06a67593 docs: remove outdated Nginx configuration from reverse proxy documentation
- Eliminated the Nginx HTTP configuration section for reverse proxies without HTTPS/SSL to streamline the documentation.
- Maintained focus on the SECURE_SITE variable and Docker Compose setup for clarity in reverse proxy configurations.
2025-06-18 12:14:56 -03:00
Daniel Luiz Alves
9682f96905 docs: reverse proxy documentation to streamline Docker Compose example
- Removed outdated Docker Compose configuration for the Palmr service.
- Retained the SECURE_SITE environment variable setting for clarity.
- Updated documentation to emphasize HTTP security considerations.
2025-06-18 12:14:05 -03:00
Daniel Luiz Alves
d2c69c3b36 feat: Add SECURE_SITE configuration and reverse proxy documentation
- Introduced the SECURE_SITE environment variable to control cookie security settings based on deployment context.
- Updated Dockerfile to log SECURE_SITE status during application startup.
- Enhanced documentation with a new guide on reverse proxy configuration, detailing the use of SECURE_SITE for secure cookie handling.
- Adjusted authentication and email services to utilize SECURE_SITE for secure connections.
- Updated frontend components to set cookie security based on the current protocol.
2025-06-18 12:10:54 -03:00
18 changed files with 416 additions and 50 deletions

View File

@@ -132,6 +132,7 @@ set -e
echo "Starting Palmr Application..." echo "Starting Palmr Application..."
echo "Storage Mode: \${ENABLE_S3:-false}" echo "Storage Mode: \${ENABLE_S3:-false}"
echo "Secure Site: \${SECURE_SITE:-false}"
echo "Database: SQLite" echo "Database: SQLite"
# Set global environment variables # Set global environment variables

View File

@@ -15,6 +15,7 @@
"configuring-smtp", "configuring-smtp",
"available-languages", "available-languages",
"uid-gid-configuration", "uid-gid-configuration",
"reverse-proxy-configuration",
"password-reset-without-smtp", "password-reset-without-smtp",
"oidc-authentication", "oidc-authentication",
"---Developers---", "---Developers---",

View File

@@ -56,6 +56,7 @@ services:
environment: environment:
- ENABLE_S3=false - ENABLE_S3=false
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY - 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: ports:
- "5487:5487" # Web interface - "5487:5487" # Web interface
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY) - "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
@@ -91,6 +92,7 @@ services:
environment: environment:
- ENABLE_S3=false - ENABLE_S3=false
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY - 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 # Optional: Set custom UID/GID for file permissions
# - PALMR_UID=1000 # - PALMR_UID=1000
# - PALMR_GID=1000 # - PALMR_GID=1000
@@ -121,9 +123,12 @@ Configure Palmr. behavior through environment variables:
| ---------------- | ------- | ------------------------------------------------------- | | ---------------- | ------- | ------------------------------------------------------- |
| `ENABLE_S3` | `false` | Enable S3-compatible storage | | `ENABLE_S3` | `false` | Enable S3-compatible storage |
| `ENCRYPTION_KEY` | - | **Required**: Minimum 32 characters for file encryption | | `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. > **⚠️ 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 ### Generate Secure Encryption Keys
Need a strong key for `ENCRYPTION_KEY`? Use our built-in generator to create cryptographically secure keys: Need a strong key for `ENCRYPTION_KEY`? Use our built-in generator to create cryptographically secure keys:

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

View File

@@ -11,6 +11,7 @@ const envSchema = z.object({
S3_REGION: z.string().optional(), S3_REGION: z.string().optional(),
S3_BUCKET_NAME: z.string().optional(), S3_BUCKET_NAME: z.string().optional(),
S3_FORCE_PATH_STYLE: z.union([z.literal("true"), z.literal("false")]).default("false"), 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"), DATABASE_URL: z.string().optional().default("file:/app/server/prisma/palmr.db"),
}); });

View File

@@ -4,7 +4,21 @@ import { FastifyReply, FastifyRequest } from "fastify";
import fs from "fs"; import fs from "fs";
import path from "path"; 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)) { if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true }); fs.mkdirSync(uploadsDir, { recursive: true });
} }

View File

@@ -1,3 +1,4 @@
import { env } from "../../env";
import { LoginSchema, RequestPasswordResetSchema, createResetPasswordSchema } from "./dto"; import { LoginSchema, RequestPasswordResetSchema, createResetPasswordSchema } from "./dto";
import { AuthService } from "./service"; import { AuthService } from "./service";
import { FastifyReply, FastifyRequest } from "fastify"; import { FastifyReply, FastifyRequest } from "fastify";
@@ -17,8 +18,8 @@ export class AuthController {
reply.setCookie("token", token, { reply.setCookie("token", token, {
httpOnly: true, httpOnly: true,
path: "/", path: "/",
secure: false, secure: env.SECURE_SITE === "true" ? true : false,
sameSite: "strict", sameSite: env.SECURE_SITE === "true" ? "lax" : "strict",
}); });
return reply.send({ user }); return reply.send({ user });

View File

@@ -1,3 +1,4 @@
import { env } from "../../env";
import { ConfigService } from "../config/service"; import { ConfigService } from "../config/service";
import nodemailer from "nodemailer"; import nodemailer from "nodemailer";
@@ -13,7 +14,7 @@ export class EmailService {
return nodemailer.createTransport({ return nodemailer.createTransport({
host: await this.configService.getValue("smtpHost"), host: await this.configService.getValue("smtpHost"),
port: Number(await this.configService.getValue("smtpPort")), port: Number(await this.configService.getValue("smtpPort")),
secure: false, secure: env.SECURE_SITE === "true" ? true : false,
auth: { auth: {
user: await this.configService.getValue("smtpUser"), user: await this.configService.getValue("smtpUser"),
pass: await this.configService.getValue("smtpPass"), pass: await this.configService.getValue("smtpPass"),

View File

@@ -2,12 +2,35 @@ import { ConfigService } from "../config/service";
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
import { exec } from "child_process"; import { exec } from "child_process";
import { promisify } from "util"; import { promisify } from "util";
import fs from 'node:fs';
const execAsync = promisify(exec); const execAsync = promisify(exec);
const prisma = new PrismaClient(); const prisma = new PrismaClient();
export class StorageService { export class StorageService {
private configService = new ConfigService(); 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( async getDiskSpace(
userId?: string, userId?: string,
@@ -20,11 +43,14 @@ export class StorageService {
}> { }> {
try { try {
if (isAdmin) { if (isAdmin) {
const isDocker = this._isDocker();
const pathToCheck = isDocker ? "/app/server/uploads" : ".";
const command = process.platform === "win32" const command = process.platform === "win32"
? "wmic logicaldisk get size,freespace,caption" ? "wmic logicaldisk get size,freespace,caption"
: process.platform === "darwin" : process.platform === "darwin"
? "df -k ." ? `df -k ${pathToCheck}`
: "df -B1 ."; : `df -B1 ${pathToCheck}`;
const { stdout } = await execAsync(command); const { stdout } = await execAsync(command);
let total = 0; let total = 0;

View File

@@ -14,6 +14,7 @@ export async function userRoutes(app: FastifyInstance) {
const usersCount = await prisma.user.count(); const usersCount = await prisma.user.count();
if (usersCount > 0) { if (usersCount > 0) {
try {
await request.jwtVerify(); await request.jwtVerify();
if (!request.user.isAdmin) { if (!request.user.isAdmin) {
return reply return reply
@@ -21,14 +22,19 @@ export async function userRoutes(app: FastifyInstance) {
.send({ error: "Access restricted to administrators" }) .send({ error: "Access restricted to administrators" })
.description("Access restricted to administrators"); .description("Access restricted to administrators");
} }
} } catch (authErr) {
} catch (err) { console.error(authErr);
console.error(err);
return reply return reply
.status(401) .status(401)
.send({ error: "Unauthorized: a valid token is required to access this resource." }) .send({ error: "Unauthorized: a valid token is required to access this resource." })
.description("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(500).send({ error: "Internal server error" }).description("Internal server error");
}
}; };
const createRegisterSchema = async () => { const createRegisterSchema = async () => {

View File

@@ -9,16 +9,31 @@ import { pipeline } from "stream/promises";
export class FilesystemStorageProvider implements StorageProvider { export class FilesystemStorageProvider implements StorageProvider {
private static instance: FilesystemStorageProvider; private static instance: FilesystemStorageProvider;
private uploadsDir = path.join(process.cwd(), "uploads"); private uploadsDir: string;
private encryptionKey = env.ENCRYPTION_KEY; private encryptionKey = env.ENCRYPTION_KEY;
private uploadTokens = new Map<string, { objectName: string; expiresAt: number }>(); private uploadTokens = new Map<string, { objectName: string; expiresAt: number }>();
private downloadTokens = new Map<string, { objectName: string; expiresAt: number; fileName?: string }>(); private downloadTokens = new Map<string, { objectName: string; expiresAt: number; fileName?: string }>();
private constructor() { private constructor() {
this.uploadsDir = this.isDocker() ? "/app/server/uploads" : path.join(process.cwd(), "uploads");
this.ensureUploadsDir(); this.ensureUploadsDir();
setInterval(() => this.cleanExpiredTokens(), 5 * 60 * 1000); 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 { public static getInstance(): FilesystemStorageProvider {
if (!FilesystemStorageProvider.instance) { if (!FilesystemStorageProvider.instance) {
FilesystemStorageProvider.instance = new FilesystemStorageProvider(); FilesystemStorageProvider.instance = new FilesystemStorageProvider();

View File

@@ -13,6 +13,7 @@ import { storageRoutes } from "./modules/storage/routes";
import { userRoutes } from "./modules/user/routes"; import { userRoutes } from "./modules/user/routes";
import fastifyMultipart from "@fastify/multipart"; import fastifyMultipart from "@fastify/multipart";
import fastifyStatic from "@fastify/static"; import fastifyStatic from "@fastify/static";
import * as fsSync from "fs";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import crypto from "node:crypto"; import crypto from "node:crypto";
import path from "path"; import path from "path";
@@ -26,21 +27,36 @@ if (typeof global.crypto === "undefined") {
} }
async function ensureDirectories() { async function ensureDirectories() {
const uploadsDir = path.join(process.cwd(), "uploads"); // Use /app/server paths in Docker, current directory for local development
const tempChunksDir = path.join(process.cwd(), "temp-chunks"); 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 { try {
await fs.access(uploadsDir); await fs.access(uploadsDir);
} catch { } catch {
await fs.mkdir(uploadsDir, { recursive: true }); await fs.mkdir(uploadsDir, { recursive: true });
console.log("📁 Created uploads directory"); console.log(`📁 Created uploads directory: ${uploadsDir}`);
} }
try { try {
await fs.access(tempChunksDir); await fs.access(tempChunksDir);
} catch { } catch {
await fs.mkdir(tempChunksDir, { recursive: true }); 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") { 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, { await app.register(fastifyStatic, {
root: path.join(process.cwd(), "uploads"), root: uploadsPath,
prefix: "/uploads/", prefix: "/uploads/",
decorateReply: false, decorateReply: false,
}); });

View File

@@ -3,12 +3,15 @@ import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest, { params }: { params: Promise<{ shareId: string }> }) { export async function POST(req: NextRequest, { params }: { params: Promise<{ shareId: string }> }) {
const cookieHeader = req.headers.get("cookie"); const cookieHeader = req.headers.get("cookie");
const { shareId } = await params; 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", method: "POST",
headers: { headers: {
"Content-Type": "application/json",
cookie: cookieHeader || "", cookie: cookieHeader || "",
}, },
body: body,
redirect: "manual", redirect: "manual",
}); });

View File

@@ -14,8 +14,8 @@ export default function AuthCallbackPage() {
if (token) { if (token) {
Cookies.set("token", token, { Cookies.set("token", token, {
path: "/", path: "/",
secure: false, secure: window.location.protocol === "https:",
sameSite: "strict", sameSite: window.location.protocol === "https:" ? "lax" : "strict",
httpOnly: false, httpOnly: false,
}); });

View File

@@ -49,6 +49,17 @@ export function useLogin() {
useEffect(() => { useEffect(() => {
const checkAuth = async () => { const checkAuth = async () => {
try { 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(); const userResponse = await getCurrentUser();
if (!userResponse?.data?.user) { if (!userResponse?.data?.user) {
throw new Error("No user data"); throw new Error("No user data");

View File

@@ -49,7 +49,7 @@ export function LanguageSwitcher() {
maxAge: COOKIE_MAX_AGE, maxAge: COOKIE_MAX_AGE,
path: "/", path: "/",
sameSite: "lax", sameSite: "lax",
secure: false, secure: window.location.protocol === "https:",
}); });
router.refresh(); router.refresh();

View File

@@ -41,6 +41,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
useEffect(() => { useEffect(() => {
const checkAuth = async () => { const checkAuth = async () => {
try { 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(); const response = await getCurrentUser();
if (!response?.data?.user) { if (!response?.data?.user) {
throw new Error("No user data"); throw new Error("No user data");

View File

@@ -36,29 +36,49 @@ echo "💾 Database: $DATABASE_URL"
echo "📁 Creating data directories..." echo "📁 Creating data directories..."
mkdir -p /app/server/prisma /app/server/uploads /app/server/temp-chunks /app/server/uploads/logo 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) # Check if it's a first run (no database file exists)
if [ ! -f "/app/server/prisma/palmr.db" ]; then if [ ! -f "/app/server/prisma/palmr.db" ]; then
echo "🚀 First run detected - setting up database..." 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..." echo "🗄️ Creating database schema..."
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 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..." echo "🌱 Seeding database..."
if [ "$(id -u)" = "0" ]; then
su-exec $TARGET_UID:$TARGET_GID node ./prisma/seed.js
else
node ./prisma/seed.js node ./prisma/seed.js
fi
echo "✅ Database setup completed!" echo "✅ Database setup completed!"
else else
echo "♻️ Existing database found" 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..." echo "🔧 Checking for schema updates..."
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 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..." echo "🔍 Verifying database configurations..."
CONFIG_COUNT=$(node -e " CONFIG_COUNT=$(
if [ "$(id -u)" = "0" ]; then
su-exec $TARGET_UID:$TARGET_GID node -e "
const { PrismaClient } = require('@prisma/client'); const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient(); const prisma = new PrismaClient();
prisma.appConfig.count() prisma.appConfig.count()
@@ -70,12 +90,32 @@ else
console.log(0); console.log(0);
process.exit(0); process.exit(0);
}); });
" 2>/dev/null || echo "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 if [ "$CONFIG_COUNT" -eq "0" ]; then
echo "🌱 No configurations found, running seed..." echo "🌱 No configurations found, running seed..."
# Always run seed from application directory where node_modules is available # 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 node ./prisma/seed.js
fi
else else
echo "✅ Found $CONFIG_COUNT configurations" echo "✅ Found $CONFIG_COUNT configurations"
fi fi