Update layout, settings, and bulk upload password forcing

This commit is contained in:
2026-09-25 12:03:01 +05:30
parent e12c3fb8eb
commit ba4ab2049b
5 changed files with 144 additions and 10 deletions

View File

@@ -223,6 +223,8 @@ def change_password(payload: ChangePasswordRequest, session: Session = Depends(g
hashed_password = bcrypt.hashpw(payload.new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') hashed_password = bcrypt.hashpw(payload.new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
current_user.password_hash = hashed_password current_user.password_hash = hashed_password
if current_user.school == "BULK_REGISTERED":
current_user.school = None
session.add(current_user) session.add(current_user)
session.commit() session.commit()
return {"success": True, "message": "Password updated successfully"} return {"success": True, "message": "Password updated successfully"}
@@ -273,6 +275,7 @@ async def bulk_register_students(file: UploadFile = File(...), session: Session
tier=tier, tier=tier,
student_class=models.ClassEnum.TWELFTH, student_class=models.ClassEnum.TWELFTH,
medium=models.MediumEnum.ENGLISH, medium=models.MediumEnum.ENGLISH,
school="BULK_REGISTERED",
city="Unknown", city="Unknown",
state="Unknown" state="Unknown"
) )
@@ -356,10 +359,8 @@ def register(payload: RegisterPayload, session: Session = Depends(get_session)):
student_class = class_map.get(payload.student_class or "12", models.ClassEnum.TWELFTH) student_class = class_map.get(payload.student_class or "12", models.ClassEnum.TWELFTH)
medium = medium_map.get(payload.medium or "English", models.MediumEnum.ENGLISH) medium = medium_map.get(payload.medium or "English", models.MediumEnum.ENGLISH)
# Calculate trial_end based on admin settings # Set trial_end directly to 15 days
trial_end = None trial_end = datetime.utcnow() + timedelta(days=15)
if settings.enable_free_trial and settings.trial_duration_days > 0:
trial_end = datetime.utcnow() + timedelta(days=settings.trial_duration_days)
new_user = models.User( new_user = models.User(
name=payload.name, name=payload.name,

View File

@@ -12,6 +12,7 @@ import { StreakCounter } from "@/components/StreakCounter"
import { AITutorChat } from "@/components/AI/AITutorChat" import { AITutorChat } from "@/components/AI/AITutorChat"
import { useActiveUser, DEMO_USERS, isDemoUser } from "@/hooks/useActiveUser" import { useActiveUser, DEMO_USERS, isDemoUser } from "@/hooks/useActiveUser"
import { StreakOnboardingModal } from "@/components/Dashboard/StreakOnboardingModal" import { StreakOnboardingModal } from "@/components/Dashboard/StreakOnboardingModal"
import { BulkUserPasswordModal } from "@/components/Dashboard/BulkUserPasswordModal"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import MaintenanceScreen from "@/components/MaintenanceScreen" import MaintenanceScreen from "@/components/MaintenanceScreen"
@@ -160,14 +161,14 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
active={pathname.startsWith("/dashboard/cbt")} active={pathname.startsWith("/dashboard/cbt")}
isLocked={!hasAccessTo("weekly-test-series")} isLocked={!hasAccessTo("weekly-test-series")}
/> />
<NavItem {/* <NavItem
section="pro" section="pro"
href="/dashboard/dpp" href="/dashboard/dpp"
icon={<FileText size={17} />} icon={<FileText size={17} />}
label="Daily Practice (DPP)" label="Daily Practice (DPP)"
active={pathname.startsWith("/dashboard/dpp")} active={pathname.startsWith("/dashboard/dpp")}
isLocked={!hasAccessTo("chapter-wise-progress-tracking")} isLocked={!hasAccessTo("chapter-wise-progress-tracking")}
/> /> */}
<NavItem <NavItem
section="pro" section="pro"
@@ -422,7 +423,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
{trialInfo.isTrialActive && ( {trialInfo.isTrialActive && (
<div className="bg-primary text-primary-foreground px-4 py-2.5 text-center text-sm font-bold flex justify-center items-center gap-2 shadow-sm shrink-0 z-30"> <div className="bg-primary text-primary-foreground px-4 py-2.5 text-center text-sm font-bold flex justify-center items-center gap-2 shadow-sm shrink-0 z-30">
<span className="size-2 rounded-full bg-primary-foreground animate-pulse"></span> <span className="size-2 rounded-full bg-primary-foreground animate-pulse"></span>
{trialInfo.trialDaysRemaining} Days Remaining out of {trialInfo.trialTotalDays} Days in Free Trial 15 Days Free Trial - Day {trialInfo.trialDayNumber}
</div> </div>
)} )}

View File

@@ -5,7 +5,7 @@ import { useEffect, useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useActiveUser } from "@/hooks/useActiveUser"; import { useActiveUser } from "@/hooks/useActiveUser";
import { User as UserIcon, Upload, CheckCircle2, Settings } from "lucide-react"; import { User as UserIcon, Upload, CheckCircle2, Settings, Eye, EyeOff } from "lucide-react";
export default function SettingsPage() { export default function SettingsPage() {
const { userId, user, refreshUser, loading: userLoading } = useActiveUser(); const { userId, user, refreshUser, loading: userLoading } = useActiveUser();
@@ -74,6 +74,9 @@ export default function SettingsPage() {
const [newPassword, setNewPassword] = useState(""); const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState("");
const [passwordSaving, setPasswordSaving] = useState(false); const [passwordSaving, setPasswordSaving] = useState(false);
const [showCurrent, setShowCurrent] = useState(false);
const [showNew, setShowNew] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const handleChangePassword = async () => { const handleChangePassword = async () => {
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {

View File

@@ -0,0 +1,129 @@
"use client";
import { useState } from "react";
import { Lock, Eye, EyeOff } from "lucide-react";
import { fetchWithAuth } from "@/lib/apiClient";
import { Button } from "@/components/ui/button";
interface BulkUserPasswordModalProps {
isOpen: boolean;
onSuccess: () => void;
}
export function BulkUserPasswordModal({ isOpen, onSuccess }: BulkUserPasswordModalProps) {
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [showCurrent, setShowCurrent] = useState(false);
const [showNew, setShowNew] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
if (!isOpen) return null;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (newPassword !== confirmPassword) {
setError("New passwords do not match.");
return;
}
setSaving(true);
setError("");
try {
const res = await fetchWithAuth("/api/auth/change-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword,
}),
});
const data = await res.json();
if (res.ok) {
onSuccess();
} else {
setError("Failed: " + (data.detail || "Unknown error"));
}
} catch (err) {
setError("Network error. Could not update password.");
} finally {
setSaving(false);
}
};
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm animate-in fade-in zoom-in-95 duration-300">
<div className="bg-background border border-border rounded-2xl w-full max-w-md p-8 shadow-2xl space-y-6">
<div className="flex flex-col items-center text-center space-y-2">
<div className="w-16 h-16 rounded-full bg-[var(--cyan-ai-10)] flex items-center justify-center mb-2 shadow-[0_0_20px_-5px_var(--cyan-ai)]">
<Lock className="w-8 h-8 text-[var(--cyan-ai)]" />
</div>
<h2 className="text-2xl font-black">Set Your Password</h2>
<p className="text-muted-foreground text-sm">
Welcome! Since your account was created by the admin, please set a new personal password to continue.
</p>
</div>
{error && <p className="text-red-500 text-sm font-semibold text-center">{error}</p>}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2 text-left">
<label className="text-sm font-bold">Current Password (Given by Admin)</label>
<div className="relative">
<input
type={showCurrent ? "text" : "password"}
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 pr-10 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai)]"
required
/>
<button type="button" onClick={() => setShowCurrent(!showCurrent)} className="absolute right-3 top-2.5 text-muted-foreground hover:text-foreground">
{showCurrent ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
<div className="space-y-2 text-left">
<label className="text-sm font-bold">New Password</label>
<div className="relative">
<input
type={showNew ? "text" : "password"}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 pr-10 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai)]"
required
minLength={8}
/>
<button type="button" onClick={() => setShowNew(!showNew)} className="absolute right-3 top-2.5 text-muted-foreground hover:text-foreground">
{showNew ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
<div className="space-y-2 text-left">
<label className="text-sm font-bold">Confirm New Password</label>
<div className="relative">
<input
type={showConfirm ? "text" : "password"}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 pr-10 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan-ai)]"
required
minLength={8}
/>
<button type="button" onClick={() => setShowConfirm(!showConfirm)} className="absolute right-3 top-2.5 text-muted-foreground hover:text-foreground">
{showConfirm ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
<Button type="submit" disabled={saving || !currentPassword || !newPassword || !confirmPassword} className="w-full bg-[var(--cyan-ai)] hover:bg-cyan-400 text-black font-bold py-3 mt-4">
{saving ? "Saving..." : "Set Password & Continue"}
</Button>
</form>
</div>
</div>
);
}

View File

@@ -60,8 +60,8 @@ export function computeTrialInfo(user: UserProfile | null, globalSettings: any):
if (!user || !globalSettings) return defaultResult; if (!user || !globalSettings) return defaultResult;
const trialDays: number = globalSettings.trial_duration_days ?? 15; const trialDays: number = 15;
const trialEnabled: boolean = globalSettings.enable_free_trial ?? true; const trialEnabled: boolean = true;
// Only apply trial logic to Free tier users (not already paid Pro/AI Coach) // Only apply trial logic to Free tier users (not already paid Pro/AI Coach)
if (user.tier !== "Free") { if (user.tier !== "Free") {