mirror of
https://github.com/kyantech/Palmr.git
synced 2025-11-01 12:33:34 +00:00
Compare commits
17 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 });
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { env } from "../../env";
|
||||
import { ConfigService } from "../config/service";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
@@ -13,7 +14,7 @@ export class EmailService {
|
||||
return nodemailer.createTransport({
|
||||
host: await this.configService.getValue("smtpHost"),
|
||||
port: Number(await this.configService.getValue("smtpPort")),
|
||||
secure: false,
|
||||
secure: env.SECURE_SITE === "true" ? true : false,
|
||||
auth: {
|
||||
user: await this.configService.getValue("smtpUser"),
|
||||
pass: await this.configService.getValue("smtpPass"),
|
||||
|
||||
@@ -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 command = process.platform === "win32"
|
||||
? "wmic logicaldisk get size,freespace,caption"
|
||||
: process.platform === "darwin"
|
||||
? "df -k ."
|
||||
: "df -B1 .";
|
||||
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 ${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