Update layout, settings, and bulk upload password forcing
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
129
src/components/Dashboard/BulkUserPasswordModal.tsx
Normal file
129
src/components/Dashboard/BulkUserPasswordModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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") {
|
||||||
|
|||||||
Reference in New Issue
Block a user