mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 16:14:18 +00:00
Compare commits
9 Commits
v3.1.7-bet
...
v3.1.8-bet
Author | SHA1 | Date | |
---|---|---|---|
|
e7ae7833ad | ||
|
22f34f6f81 | ||
|
29efe0a10e | ||
|
965c64b468 | ||
|
ce57cda672 | ||
|
a59857079e | ||
|
9ae2a0c628 | ||
|
f2c514cd82 | ||
|
6755230c53 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "palmr-docs",
|
||||
"version": "3.1.7-beta",
|
||||
"version": "3.1.8-beta",
|
||||
"description": "Docs for Palmr",
|
||||
"private": true,
|
||||
"author": "Daniel Luiz Alves <daniel@kyantech.com.br>",
|
||||
|
@@ -12,7 +12,6 @@ import {
|
||||
LayoutIcon,
|
||||
LockIcon,
|
||||
MousePointer,
|
||||
RadioIcon,
|
||||
RocketIcon,
|
||||
SearchIcon,
|
||||
TimerIcon,
|
||||
@@ -82,23 +81,6 @@ function Hero() {
|
||||
<Link href={docsLink}>Documentation</Link>
|
||||
</div>
|
||||
</PulsatingButton>
|
||||
<RippleButton
|
||||
onClick={() => {
|
||||
const demoId = `${Math.random().toString(36).substr(2, 9)}`;
|
||||
const token = `${Math.random().toString(36).substr(2, 12)}`;
|
||||
|
||||
sessionStorage.setItem("demo_token", token);
|
||||
sessionStorage.setItem("demo_id", demoId);
|
||||
sessionStorage.setItem("demo_expires", (Date.now() + 5 * 60 * 1000).toString());
|
||||
|
||||
window.location.href = `/demo?id=${demoId}&token=${token}`;
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<RadioIcon size={18} />
|
||||
Live Demo
|
||||
</div>
|
||||
</RippleButton>
|
||||
<RippleButton>
|
||||
<a
|
||||
href="https://github.com/kyantech/Palmr"
|
||||
|
@@ -1,225 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Palmtree } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
import { BackgroundLights } from "@/components/ui/background-lights";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface DemoStatus {
|
||||
status: "waiting" | "ready";
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
interface CreateDemoResponse {
|
||||
message: string;
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
function DemoClientInner() {
|
||||
const searchParams = useSearchParams();
|
||||
const demoId = searchParams.get("id");
|
||||
const token = searchParams.get("token");
|
||||
|
||||
const [status, setStatus] = useState<DemoStatus | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const validateAccess = () => {
|
||||
const storedToken = sessionStorage.getItem("demo_token");
|
||||
const storedId = sessionStorage.getItem("demo_id");
|
||||
const expiresAt = sessionStorage.getItem("demo_expires");
|
||||
|
||||
if (!demoId || !token || !storedToken || !storedId || !expiresAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (token !== storedToken || demoId !== storedId || Date.now() > parseInt(expiresAt)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!validateAccess()) {
|
||||
setError("Unauthorized access. Please use the Live Demo button to access this page.");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const createDemo = async () => {
|
||||
try {
|
||||
const response = await fetch("https://palmr-demo-manager.kyantech.com.br/create-demo", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
palmr_demo_instance_id: demoId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create demo");
|
||||
}
|
||||
|
||||
const data: CreateDemoResponse = await response.json();
|
||||
console.log("Demo creation response:", data);
|
||||
} catch (err) {
|
||||
console.error("Error creating demo:", err);
|
||||
setError("Failed to create demo. Please try again.");
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const checkStatus = async () => {
|
||||
try {
|
||||
const response = await fetch(`https://palmr-demo-manager.kyantech.com.br/status/${demoId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to check demo status");
|
||||
}
|
||||
|
||||
const data: DemoStatus = await response.json();
|
||||
setStatus(data);
|
||||
|
||||
if (data.status === "ready" && data.url) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error checking status:", err);
|
||||
setError("Failed to check demo status. Please try again.");
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
createDemo();
|
||||
|
||||
const interval = setInterval(checkStatus, 5000); // Check every 5 seconds
|
||||
|
||||
checkStatus();
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
sessionStorage.removeItem("demo_token");
|
||||
sessionStorage.removeItem("demo_id");
|
||||
sessionStorage.removeItem("demo_expires");
|
||||
};
|
||||
}, [demoId, token]);
|
||||
|
||||
const handleGoToDemo = () => {
|
||||
if (status?.url) {
|
||||
window.open(status.url, "_blank");
|
||||
}
|
||||
window.location.href = "/";
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-background">
|
||||
<BackgroundLights />
|
||||
<div className="relative flex flex-col items-center justify-center h-full">
|
||||
<div className="text-center space-y-6 max-w-md">
|
||||
<h1 className="text-2xl font-bold text-destructive">Error</h1>
|
||||
<p className="text-muted-foreground">{error}</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
sessionStorage.removeItem("demo_token");
|
||||
sessionStorage.removeItem("demo_id");
|
||||
sessionStorage.removeItem("demo_expires");
|
||||
window.location.href = "/";
|
||||
}}
|
||||
>
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-background">
|
||||
<BackgroundLights />
|
||||
<div className="flex flex-col items-center gap-6 text-center h-full justify-center">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-2xl font-bold">Your demo is being generated, please wait...</h1>
|
||||
<p className="text-muted-foreground max-w-lg">
|
||||
This demo will be available for 30 minutes for testing. After that, all data will be permanently deleted
|
||||
and become inaccessible. You can test Palmr. with a 200MB storage limit.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-background">
|
||||
<BackgroundLights />
|
||||
<div className="relative flex flex-col items-center justify-center h-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="container mx-auto max-w-7xl px-6 flex-grow"
|
||||
>
|
||||
<section className="relative flex flex-col items-center justify-center gap-6 m-auto h-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
className="inline-block max-w-xl text-center justify-center"
|
||||
>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<motion.span
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.4, duration: 0.5 }}
|
||||
className="text-4xl lg:text-3xl font-semibold tracking-tight text-primary"
|
||||
>
|
||||
Your demo is ready!
|
||||
</motion.span>
|
||||
<motion.span
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.6, duration: 0.5 }}
|
||||
className="text-3xl leading-9 font-semibold tracking-tight"
|
||||
>
|
||||
Click the button below to test
|
||||
</motion.span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.8 }}
|
||||
className="flex flex-col items-center gap-6"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 1.2, duration: 0.5 }}
|
||||
>
|
||||
<Button onClick={handleGoToDemo} className="flex items-center gap-2 px-8 py-4 text-lg">
|
||||
<Palmtree className="h-5 w-5" />
|
||||
Go to Palmr. Demo
|
||||
</Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</section>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DemoClient() {
|
||||
return <DemoClientInner />;
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense } from "react";
|
||||
|
||||
import DemoClient from "./components/demo-client";
|
||||
|
||||
export default function DemoPage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<DemoClient />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "palmr-api",
|
||||
"version": "3.1.7-beta",
|
||||
"version": "3.1.8-beta",
|
||||
"description": "API for Palmr",
|
||||
"private": true,
|
||||
"author": "Daniel Luiz Alves <daniel@kyantech.com.br>",
|
||||
|
@@ -14,7 +14,6 @@ const envSchema = z.object({
|
||||
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"),
|
||||
DEMO_MODE: z.union([z.literal("true"), z.literal("false")]).default("false"),
|
||||
});
|
||||
|
||||
export const env = envSchema.parse(process.env);
|
||||
|
@@ -113,14 +113,21 @@ export class AuthController {
|
||||
|
||||
async getCurrentUser(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const userId = (request as any).user?.userId;
|
||||
let userId: string | null = null;
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
userId = (request as any).user?.userId;
|
||||
} catch (err) {
|
||||
return reply.send({ user: null });
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." });
|
||||
return reply.send({ user: null });
|
||||
}
|
||||
|
||||
const user = await this.authService.getUserById(userId);
|
||||
if (!user) {
|
||||
return reply.status(404).send({ error: "User not found" });
|
||||
return reply.send({ user: null });
|
||||
}
|
||||
|
||||
return reply.send({ user });
|
||||
|
@@ -153,9 +153,10 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
tags: ["Authentication"],
|
||||
operationId: "getCurrentUser",
|
||||
summary: "Get Current User",
|
||||
description: "Returns the current authenticated user's information",
|
||||
description: "Returns the current authenticated user's information or null if not authenticated",
|
||||
response: {
|
||||
200: z.object({
|
||||
200: z.union([
|
||||
z.object({
|
||||
user: z.object({
|
||||
id: z.string().describe("User ID"),
|
||||
firstName: z.string().describe("User first name"),
|
||||
@@ -169,17 +170,12 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
updatedAt: z.date().describe("User last update date"),
|
||||
}),
|
||||
}),
|
||||
401: z.object({ error: z.string().describe("Error message") }),
|
||||
z.object({
|
||||
user: z.null().describe("No user when not authenticated"),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
preValidation: async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." });
|
||||
}
|
||||
},
|
||||
},
|
||||
authController.getCurrentUser.bind(authController)
|
||||
);
|
||||
|
@@ -56,17 +56,7 @@ export class FileController {
|
||||
});
|
||||
}
|
||||
|
||||
// Check if DEMO_MODE is enabled
|
||||
const isDemoMode = env.DEMO_MODE === "true";
|
||||
|
||||
let maxTotalStorage: bigint;
|
||||
if (isDemoMode) {
|
||||
// In demo mode, limit all users to 200MB
|
||||
maxTotalStorage = BigInt(200 * 1024 * 1024); // 200MB in bytes
|
||||
} else {
|
||||
// Normal behavior - use maxTotalStoragePerUser configuration
|
||||
maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
|
||||
}
|
||||
const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
|
||||
|
||||
const userFiles = await prisma.file.findMany({
|
||||
where: { userId },
|
||||
@@ -138,17 +128,7 @@ export class FileController {
|
||||
});
|
||||
}
|
||||
|
||||
// Check if DEMO_MODE is enabled
|
||||
const isDemoMode = env.DEMO_MODE === "true";
|
||||
|
||||
let maxTotalStorage: bigint;
|
||||
if (isDemoMode) {
|
||||
// In demo mode, limit all users to 200MB
|
||||
maxTotalStorage = BigInt(200 * 1024 * 1024); // 200MB in bytes
|
||||
} else {
|
||||
// Normal behavior - use maxTotalStoragePerUser configuration
|
||||
maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
|
||||
}
|
||||
const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
|
||||
|
||||
const userFiles = await prisma.file.findMany({
|
||||
where: { userId },
|
||||
|
@@ -533,17 +533,7 @@ export class ReverseShareService {
|
||||
throw new Error(`File size exceeds the maximum allowed size of ${maxSizeMB}MB`);
|
||||
}
|
||||
|
||||
// Check if DEMO_MODE is enabled
|
||||
const isDemoMode = env.DEMO_MODE === "true";
|
||||
|
||||
let maxTotalStorage: bigint;
|
||||
if (isDemoMode) {
|
||||
// In demo mode, limit all users to 200MB
|
||||
maxTotalStorage = BigInt(200 * 1024 * 1024); // 200MB in bytes
|
||||
} else {
|
||||
// Normal behavior - use maxTotalStoragePerUser configuration
|
||||
maxTotalStorage = BigInt(await configService.getValue("maxTotalStoragePerUser"));
|
||||
}
|
||||
const maxTotalStorage = BigInt(await configService.getValue("maxTotalStoragePerUser"));
|
||||
|
||||
const userFiles = await prisma.file.findMany({
|
||||
where: { userId: creatorId },
|
||||
|
@@ -324,29 +324,7 @@ export class StorageService {
|
||||
uploadAllowed: boolean;
|
||||
}> {
|
||||
try {
|
||||
const isDemoMode = env.DEMO_MODE === "true";
|
||||
|
||||
if (isAdmin) {
|
||||
if (isDemoMode) {
|
||||
const demoMaxStorage = 200 * 1024 * 1024;
|
||||
const demoMaxStorageGB = this._ensureNumber(demoMaxStorage / (1024 * 1024 * 1024), 0);
|
||||
|
||||
const userFiles = await prisma.file.findMany({
|
||||
where: { userId },
|
||||
select: { size: true },
|
||||
});
|
||||
|
||||
const totalUsedStorage = userFiles.reduce((acc, file) => acc + file.size, BigInt(0));
|
||||
const usedStorageGB = this._ensureNumber(Number(totalUsedStorage) / (1024 * 1024 * 1024), 0);
|
||||
const availableStorageGB = this._ensureNumber(demoMaxStorageGB - usedStorageGB, 0);
|
||||
|
||||
return {
|
||||
diskSizeGB: Number(demoMaxStorageGB.toFixed(2)),
|
||||
diskUsedGB: Number(usedStorageGB.toFixed(2)),
|
||||
diskAvailableGB: Number(availableStorageGB.toFixed(2)),
|
||||
uploadAllowed: availableStorageGB > 0,
|
||||
};
|
||||
} else {
|
||||
const diskInfo = await this._getDiskSpaceMultiplePaths();
|
||||
|
||||
if (!diskInfo) {
|
||||
@@ -366,28 +344,7 @@ export class StorageService {
|
||||
diskAvailableGB: Number(diskAvailableGB.toFixed(2)),
|
||||
uploadAllowed: diskAvailableGB > 0.1,
|
||||
};
|
||||
}
|
||||
} else if (userId) {
|
||||
if (isDemoMode) {
|
||||
const demoMaxStorage = 200 * 1024 * 1024;
|
||||
const demoMaxStorageGB = this._ensureNumber(demoMaxStorage / (1024 * 1024 * 1024), 0);
|
||||
|
||||
const userFiles = await prisma.file.findMany({
|
||||
where: { userId },
|
||||
select: { size: true },
|
||||
});
|
||||
|
||||
const totalUsedStorage = userFiles.reduce((acc, file) => acc + file.size, BigInt(0));
|
||||
const usedStorageGB = this._ensureNumber(Number(totalUsedStorage) / (1024 * 1024 * 1024), 0);
|
||||
const availableStorageGB = this._ensureNumber(demoMaxStorageGB - usedStorageGB, 0);
|
||||
|
||||
return {
|
||||
diskSizeGB: Number(demoMaxStorageGB.toFixed(2)),
|
||||
diskUsedGB: Number(usedStorageGB.toFixed(2)),
|
||||
diskAvailableGB: Number(availableStorageGB.toFixed(2)),
|
||||
uploadAllowed: availableStorageGB > 0,
|
||||
};
|
||||
} else {
|
||||
const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
|
||||
const maxStorageGB = this._ensureNumber(Number(maxTotalStorage) / (1024 * 1024 * 1024), 10);
|
||||
|
||||
@@ -407,7 +364,6 @@ export class StorageService {
|
||||
uploadAllowed: availableStorageGB > 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("User ID is required for non-admin users");
|
||||
} catch (error) {
|
||||
|
@@ -455,6 +455,11 @@
|
||||
},
|
||||
"pageTitle": "الملف الشخصي"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "مشاركة رمز QR",
|
||||
"description": "امسح رمز QR هذا للوصول إلى الرابط.",
|
||||
"download": "تحميل رمز QR"
|
||||
},
|
||||
"quickAccess": {
|
||||
"files": {
|
||||
"title": "ملفاتي",
|
||||
@@ -1746,10 +1751,5 @@
|
||||
"passwordRequired": "كلمة المرور مطلوبة",
|
||||
"nameRequired": "الاسم مطلوب",
|
||||
"required": "هذا الحقل مطلوب"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "مشاركة رمز QR",
|
||||
"description": "امسح رمز QR هذا للوصول إلى الرابط.",
|
||||
"download": "تحميل رمز QR"
|
||||
}
|
||||
}
|
@@ -455,6 +455,11 @@
|
||||
},
|
||||
"pageTitle": "Profil"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "QR-Code teilen",
|
||||
"description": "Scannen Sie diesen QR-Code, um auf den Link zuzugreifen.",
|
||||
"download": "QR-Code herunterladen"
|
||||
},
|
||||
"quickAccess": {
|
||||
"files": {
|
||||
"title": "Meine Dateien",
|
||||
@@ -1744,10 +1749,5 @@
|
||||
"passwordRequired": "Passwort ist erforderlich",
|
||||
"nameRequired": "Name ist erforderlich",
|
||||
"required": "Dieses Feld ist erforderlich"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "QR-Code teilen",
|
||||
"description": "Scannen Sie diesen QR-Code, um auf den Link zuzugreifen.",
|
||||
"download": "QR-Code herunterladen"
|
||||
}
|
||||
}
|
@@ -455,6 +455,11 @@
|
||||
},
|
||||
"pageTitle": "Perfil"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "Compartir Código QR",
|
||||
"description": "Escanea este código QR para acceder al enlace.",
|
||||
"download": "Descargar Código QR"
|
||||
},
|
||||
"quickAccess": {
|
||||
"files": {
|
||||
"title": "Mis archivos",
|
||||
@@ -1744,10 +1749,5 @@
|
||||
"passwordRequired": "Se requiere la contraseña",
|
||||
"nameRequired": "El nombre es obligatorio",
|
||||
"required": "Este campo es obligatorio"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "Compartir Código QR",
|
||||
"description": "Escanea este código QR para acceder al enlace.",
|
||||
"download": "Descargar Código QR"
|
||||
}
|
||||
}
|
@@ -455,6 +455,11 @@
|
||||
},
|
||||
"pageTitle": "Profil"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "Code QR de Partage",
|
||||
"description": "Scannez ce code QR pour accéder au lien.",
|
||||
"download": "Télécharger le Code QR"
|
||||
},
|
||||
"quickAccess": {
|
||||
"files": {
|
||||
"title": "Mes Fichiers",
|
||||
@@ -1744,10 +1749,5 @@
|
||||
"passwordRequired": "Le mot de passe est requis",
|
||||
"nameRequired": "Nome é obrigatório",
|
||||
"required": "Este campo é obrigatório"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "Code QR de Partage",
|
||||
"description": "Scannez ce code QR pour accéder au lien.",
|
||||
"download": "Télécharger le Code QR"
|
||||
}
|
||||
}
|
@@ -455,6 +455,11 @@
|
||||
},
|
||||
"pageTitle": "प्रोफ़ाइल"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "QR कोड साझा करें",
|
||||
"description": "इस QR कोड को स्कैन करके लिंक तक पहुंच सकते हैं।",
|
||||
"download": "QR कोड डाउनलोड करें"
|
||||
},
|
||||
"quickAccess": {
|
||||
"files": {
|
||||
"title": "मेरी फाइलें",
|
||||
@@ -1744,10 +1749,5 @@
|
||||
"passwordRequired": "पासवर्ड आवश्यक है",
|
||||
"nameRequired": "नाम आवश्यक है",
|
||||
"required": "यह फ़ील्ड आवश्यक है"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "QR कोड साझा करें",
|
||||
"description": "इस QR कोड को स्कैन करके लिंक तक पहुंच सकते हैं।",
|
||||
"download": "QR कोड डाउनलोड करें"
|
||||
}
|
||||
}
|
@@ -455,6 +455,11 @@
|
||||
},
|
||||
"pageTitle": "Profilo"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "Condividi QR Code",
|
||||
"description": "Scansiona questo codice QR per accedere al link.",
|
||||
"download": "Scarica QR Code"
|
||||
},
|
||||
"quickAccess": {
|
||||
"files": {
|
||||
"title": "I Miei File",
|
||||
@@ -1744,10 +1749,5 @@
|
||||
"passwordMinLength": "La password deve contenere almeno 6 caratteri",
|
||||
"nameRequired": "Il nome è obbligatorio",
|
||||
"required": "Questo campo è obbligatorio"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "Condividi QR Code",
|
||||
"description": "Scansiona questo codice QR per accedere al link.",
|
||||
"download": "Scarica QR Code"
|
||||
}
|
||||
}
|
@@ -455,6 +455,11 @@
|
||||
},
|
||||
"pageTitle": "プロフィール"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "QRコードを共有",
|
||||
"description": "このQRコードをスキャンしてリンクにアクセスしてください。",
|
||||
"download": "QRコードをダウンロード"
|
||||
},
|
||||
"quickAccess": {
|
||||
"files": {
|
||||
"title": "マイファイル",
|
||||
@@ -1744,10 +1749,5 @@
|
||||
"passwordRequired": "パスワードは必須です",
|
||||
"nameRequired": "名前は必須です",
|
||||
"required": "このフィールドは必須です"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "QRコードを共有",
|
||||
"description": "このQRコードをスキャンしてリンクにアクセスしてください。",
|
||||
"download": "QRコードをダウンロード"
|
||||
}
|
||||
}
|
@@ -455,6 +455,11 @@
|
||||
},
|
||||
"pageTitle": "프로필"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "QR 코드 공유",
|
||||
"description": "이 QR 코드를 스캔하여 링크에 접근할 수 있습니다.",
|
||||
"download": "QR 코드 다운로드"
|
||||
},
|
||||
"quickAccess": {
|
||||
"files": {
|
||||
"title": "내 파일",
|
||||
@@ -1744,10 +1749,5 @@
|
||||
"passwordRequired": "비밀번호는 필수입니다",
|
||||
"nameRequired": "이름은 필수입니다",
|
||||
"required": "이 필드는 필수입니다"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "QR 코드 공유",
|
||||
"description": "이 QR 코드를 스캔하여 링크에 접근할 수 있습니다.",
|
||||
"download": "QR 코드 다운로드"
|
||||
}
|
||||
}
|
@@ -455,6 +455,11 @@
|
||||
},
|
||||
"pageTitle": "Profiel"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "QR Code Delen",
|
||||
"description": "Scan deze QR-code om toegang te krijgen tot de link.",
|
||||
"download": "QR Code Downloaden"
|
||||
},
|
||||
"quickAccess": {
|
||||
"files": {
|
||||
"title": "Mijn Bestanden",
|
||||
@@ -1744,10 +1749,5 @@
|
||||
"passwordMinLength": "Wachtwoord moet minimaal 6 tekens bevatten",
|
||||
"nameRequired": "Naam is verplicht",
|
||||
"required": "Dit veld is verplicht"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "QR Code Delen",
|
||||
"description": "Scan deze QR-code om toegang te krijgen tot de link.",
|
||||
"download": "QR Code Downloaden"
|
||||
}
|
||||
}
|
@@ -455,6 +455,11 @@
|
||||
},
|
||||
"pageTitle": "Profil"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "Udostępnij kod QR",
|
||||
"description": "Skanuj ten kod QR, aby uzyskać dostęp do linku.",
|
||||
"download": "Pobierz kod QR"
|
||||
},
|
||||
"quickAccess": {
|
||||
"files": {
|
||||
"title": "Moje pliki",
|
||||
@@ -1744,10 +1749,5 @@
|
||||
"passwordMinLength": "Hasło musi mieć co najmniej 6 znaków",
|
||||
"nameRequired": "Nazwa jest wymagana",
|
||||
"required": "To pole jest wymagane"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "Udostępnij kod QR",
|
||||
"description": "Skanuj ten kod QR, aby uzyskać dostęp do linku.",
|
||||
"download": "Pobierz kod QR"
|
||||
}
|
||||
}
|
@@ -455,6 +455,11 @@
|
||||
},
|
||||
"pageTitle": "Perfil"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "Compartilhar QR Code",
|
||||
"description": "Escaneie este código QR para acessar o link.",
|
||||
"download": "Baixar QR Code"
|
||||
},
|
||||
"quickAccess": {
|
||||
"files": {
|
||||
"title": "Meus Arquivos",
|
||||
@@ -1744,10 +1749,5 @@
|
||||
"lastNameRequired": "O sobrenome é necessário",
|
||||
"usernameLength": "O nome de usuário deve ter pelo menos 3 caracteres",
|
||||
"usernameSpaces": "O nome de usuário não pode conter espaços"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "Compartilhar QR Code",
|
||||
"description": "Escaneie este código QR para acessar o link.",
|
||||
"download": "Baixar QR Code"
|
||||
}
|
||||
}
|
@@ -455,6 +455,11 @@
|
||||
},
|
||||
"pageTitle": "Профиль"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "Поделиться QR-кодом",
|
||||
"description": "Отсканируйте этот QR-код, чтобы получить доступ к ссылке.",
|
||||
"download": "Скачать QR-код"
|
||||
},
|
||||
"quickAccess": {
|
||||
"files": {
|
||||
"title": "Мои файлы",
|
||||
@@ -1744,10 +1749,5 @@
|
||||
"passwordRequired": "Требуется пароль",
|
||||
"nameRequired": "Требуется имя",
|
||||
"required": "Это поле обязательно"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "Поделиться QR-кодом",
|
||||
"description": "Отсканируйте этот QR-код, чтобы получить доступ к ссылке.",
|
||||
"download": "Скачать QR-код"
|
||||
}
|
||||
}
|
@@ -455,6 +455,11 @@
|
||||
},
|
||||
"pageTitle": "Profil"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "QR Kodu Paylaş",
|
||||
"description": "Bu QR kodu tarayarak bağlantıya erişebilirsiniz.",
|
||||
"download": "QR Kodu İndir"
|
||||
},
|
||||
"quickAccess": {
|
||||
"files": {
|
||||
"title": "Benim Dosyalarım",
|
||||
@@ -1744,10 +1749,5 @@
|
||||
"passwordRequired": "Şifre gerekli",
|
||||
"nameRequired": "İsim gereklidir",
|
||||
"required": "Bu alan zorunludur"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "QR Kodu Paylaş",
|
||||
"description": "Bu QR kodu tarayarak bağlantıya erişebilirsiniz.",
|
||||
"download": "QR Kodu İndir"
|
||||
}
|
||||
}
|
@@ -455,6 +455,11 @@
|
||||
},
|
||||
"pageTitle": "个人资料"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "分享QR Code",
|
||||
"description": "扫描此QR Code以访问链接。",
|
||||
"download": "下载QR Code"
|
||||
},
|
||||
"quickAccess": {
|
||||
"files": {
|
||||
"title": "我的文件",
|
||||
@@ -1507,11 +1512,7 @@
|
||||
"copyToClipboard": "复制到剪贴板",
|
||||
"savedMessage": "我已保存备用码",
|
||||
"available": "可用备用码:{count}个",
|
||||
"instructions": [
|
||||
"• 将这些代码保存在安全的位置",
|
||||
"• 每个备用码只能使用一次",
|
||||
"• 您可以随时生成新的备用码"
|
||||
]
|
||||
"instructions": ["• 将这些代码保存在安全的位置", "• 每个备用码只能使用一次", "• 您可以随时生成新的备用码"]
|
||||
},
|
||||
"verification": {
|
||||
"title": "双重认证",
|
||||
@@ -1744,10 +1745,5 @@
|
||||
"passwordRequired": "密码为必填项",
|
||||
"nameRequired": "名称为必填项",
|
||||
"required": "此字段为必填项"
|
||||
},
|
||||
"qrCodeModal": {
|
||||
"title": "分享QR Code",
|
||||
"description": "扫描此QR Code以访问链接。",
|
||||
"download": "下载QR Code"
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "palmr-web",
|
||||
"version": "3.1.7-beta",
|
||||
"version": "3.1.8-beta",
|
||||
"description": "Frontend for Palmr",
|
||||
"private": true,
|
||||
"author": "Daniel Luiz Alves <daniel@kyantech.com.br>",
|
||||
|
@@ -4,34 +4,51 @@ import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { create } from "zustand";
|
||||
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { useSecureConfigValue } from "@/hooks/use-secure-configs";
|
||||
|
||||
interface HomeStore {
|
||||
isLoading: boolean;
|
||||
shouldShowHomePage: boolean;
|
||||
setIsLoading: (loading: boolean) => void;
|
||||
setShouldShowHomePage: (show: boolean) => void;
|
||||
}
|
||||
|
||||
const useHomeStore = create<HomeStore>((set) => ({
|
||||
isLoading: true,
|
||||
shouldShowHomePage: false,
|
||||
setIsLoading: (loading: boolean) => set({ isLoading: loading }),
|
||||
setShouldShowHomePage: (show: boolean) => set({ shouldShowHomePage: show }),
|
||||
}));
|
||||
|
||||
export function useHome() {
|
||||
const router = useRouter();
|
||||
const { isLoading, setIsLoading } = useHomeStore();
|
||||
const { isLoading, shouldShowHomePage, setIsLoading, setShouldShowHomePage } = useHomeStore();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { value: showHomePage, isLoading: configLoading } = useSecureConfigValue("showHomePage");
|
||||
|
||||
useEffect(() => {
|
||||
if (!configLoading) {
|
||||
if (isAuthenticated === true) {
|
||||
router.replace("/dashboard");
|
||||
return;
|
||||
}
|
||||
}, [isAuthenticated, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!configLoading && isAuthenticated !== null) {
|
||||
setIsLoading(false);
|
||||
|
||||
if (showHomePage !== "true") {
|
||||
router.push("/login");
|
||||
setShouldShowHomePage(false);
|
||||
} else if (isAuthenticated === false) {
|
||||
setShouldShowHomePage(true);
|
||||
}
|
||||
}
|
||||
}, [router, showHomePage, configLoading, setIsLoading]);
|
||||
}, [router, showHomePage, configLoading, isAuthenticated, setIsLoading, setShouldShowHomePage]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
shouldShowHomePage,
|
||||
};
|
||||
}
|
||||
|
@@ -7,9 +7,9 @@ import { Navbar } from "./components/navbar";
|
||||
import { useHome } from "./hooks/use-home";
|
||||
|
||||
export default function HomePage() {
|
||||
const { isLoading } = useHome();
|
||||
const { isLoading, shouldShowHomePage } = useHome();
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading || !shouldShowHomePage) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import { getLocale } from "next-intl/server";
|
||||
|
||||
import "./globals.css";
|
||||
|
||||
import { RedirectHandler } from "@/components/auth/redirect-handler";
|
||||
import { Favicon } from "@/components/layout/favicon";
|
||||
import { DynamicToaster } from "@/components/ui/dynamic-toaster";
|
||||
import { useAppInfo } from "@/contexts/app-info-context";
|
||||
@@ -39,7 +40,9 @@ export default async function RootLayout({
|
||||
<NextIntlClientProvider>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
||||
<AuthProvider>
|
||||
<RedirectHandler>
|
||||
<ShareProvider>{children}</ShareProvider>
|
||||
</RedirectHandler>
|
||||
</AuthProvider>
|
||||
<DynamicToaster />
|
||||
</ThemeProvider>
|
||||
|
@@ -34,6 +34,12 @@ export function useLogin() {
|
||||
const [passwordAuthEnabled, setPasswordAuthEnabled] = useState(true);
|
||||
const [authConfigLoading, setAuthConfigLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated === true) {
|
||||
router.replace("/dashboard");
|
||||
}
|
||||
}, [isAuthenticated, router]);
|
||||
|
||||
useEffect(() => {
|
||||
const errorParam = searchParams.get("error");
|
||||
const messageParam = searchParams.get("message");
|
||||
|
@@ -17,7 +17,7 @@ export default function LoginPage() {
|
||||
const login = useLogin();
|
||||
const { firstAccess } = useAppInfo();
|
||||
|
||||
if (login.isAuthenticated === null) {
|
||||
if (login.isAuthenticated === null || login.isAuthenticated === true) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
|
63
apps/web/src/components/auth/redirect-handler.tsx
Normal file
63
apps/web/src/components/auth/redirect-handler.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
import { LoadingScreen } from "@/components/layout/loading-screen";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
|
||||
interface RedirectHandlerProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const publicPaths = [
|
||||
"/login",
|
||||
"/forgot-password",
|
||||
"/reset-password",
|
||||
"/auth/callback",
|
||||
"/auth/oidc/callback",
|
||||
"/s/",
|
||||
"/r/",
|
||||
];
|
||||
const homePaths = ["/"];
|
||||
|
||||
export function RedirectHandler({ children }: RedirectHandlerProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated === true) {
|
||||
if (publicPaths.some((path) => pathname.startsWith(path)) || homePaths.includes(pathname)) {
|
||||
router.replace("/dashboard");
|
||||
return;
|
||||
}
|
||||
} else if (isAuthenticated === false) {
|
||||
if (!publicPaths.some((path) => pathname.startsWith(path)) && !homePaths.includes(pathname)) {
|
||||
router.replace("/login");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [isAuthenticated, pathname, router]);
|
||||
|
||||
if (isAuthenticated === null) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
if (
|
||||
isAuthenticated === true &&
|
||||
(publicPaths.some((path) => pathname.startsWith(path)) || homePaths.includes(pathname))
|
||||
) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
if (
|
||||
isAuthenticated === false &&
|
||||
!publicPaths.some((path) => pathname.startsWith(path)) &&
|
||||
!homePaths.includes(pathname)
|
||||
) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
@@ -39,11 +39,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const appInfoResponse = await getAppInfo();
|
||||
const appInfo = appInfoResponse.data;
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
if (appInfo.firstUserAccess) {
|
||||
setUser(null);
|
||||
setIsAdmin(false);
|
||||
@@ -52,8 +56,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
const response = await getCurrentUser();
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
if (!response?.data?.user) {
|
||||
throw new Error("No user data");
|
||||
setUser(null);
|
||||
setIsAdmin(false);
|
||||
setIsAuthenticated(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { isAdmin, ...userData } = response.data.user;
|
||||
@@ -62,6 +72,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
setIsAdmin(isAdmin);
|
||||
setIsAuthenticated(true);
|
||||
} catch (err) {
|
||||
if (!isMounted) return;
|
||||
|
||||
console.error(err);
|
||||
setUser(null);
|
||||
setIsAdmin(false);
|
||||
@@ -70,6 +82,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "palmr-monorepo",
|
||||
"version": "3.1.7-beta",
|
||||
"version": "3.1.8-beta",
|
||||
"description": "Palmr monorepo with Husky configuration",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.6.0",
|
||||
|
Reference in New Issue
Block a user