feat: add social features, leaderboard, auth enhancements, and admin fixes
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Implement Social Features: Shared Tasks, Global Access, Privacy Settings (Leaderboard/Searchable). - Add Leaderboard Page and API. - Enhance Auth: Support Email/Username login, explicit duplicate registration errors. - Fix: Admin login password hash regression. - Refactor: Move to wouter for routing, add Admin Dashboard and User Management. - Add Setup Wizard. - Update UI with Sidebar and Gamification elements.
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useLocation } from "wouter";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { insertUserSchema, InsertUser, loginSchema, registerSchema, LoginUser } from "@shared/schema";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { BrainCircuit } from "lucide-react";
|
||||
|
||||
export default function AuthPage() {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: settings } = useQuery<{ registration_enabled: boolean }>({
|
||||
queryKey: ["/api/settings/public"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/settings/public");
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch settings");
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
});
|
||||
|
||||
const loginMutation = useMutation({
|
||||
mutationFn: async (data: LoginUser) => {
|
||||
const res = await fetch("/api/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Invalid username or password");
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/user"] });
|
||||
toast({ title: "Welcome back!" });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast({
|
||||
title: "Login failed",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const registerMutation = useMutation({
|
||||
mutationFn: async (data: InsertUser) => {
|
||||
const res = await fetch("/api/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text || "Registration failed");
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/user"] });
|
||||
toast({ title: "Account created!" });
|
||||
},
|
||||
// Error handling is done in the form submission handler to set field errors
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen grid lg:grid-cols-2">
|
||||
<div className="hidden lg:flex flex-col justify-center items-center bg-zinc-900 p-12 text-white">
|
||||
<div className="max-w-md space-y-4 text-center">
|
||||
<div className="bg-white/10 p-4 rounded-2xl inline-block mb-4 backdrop-blur-sm">
|
||||
<BrainCircuit className="w-16 h-16 text-primary-foreground" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight">TaskFlow</h1>
|
||||
<p className="text-lg text-zinc-400">
|
||||
Master your productivity with AI-driven task management, gamified
|
||||
achievements, and intelligent focus modes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-4 bg-background">
|
||||
<Card className="w-full max-w-md shadow-xl border-border/50">
|
||||
<CardHeader className="text-center space-y-2">
|
||||
<div className="lg:hidden mx-auto bg-primary/10 p-3 rounded-xl w-fit mb-2">
|
||||
<BrainCircuit className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl font-bold">Welcome Back</CardTitle>
|
||||
<CardDescription>
|
||||
Sign in to your account
|
||||
{settings?.registration_enabled && " or create a new one"} to get started
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="login" className="space-y-6">
|
||||
<TabsList className={`grid w-full ${settings?.registration_enabled ? 'grid-cols-2' : 'grid-cols-1'}`}>
|
||||
<TabsTrigger value="login">Login</TabsTrigger>
|
||||
{settings?.registration_enabled && (
|
||||
<TabsTrigger value="register">Register</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="login">
|
||||
<AuthForm
|
||||
mode="login"
|
||||
onSubmit={(data) => loginMutation.mutate(data)}
|
||||
isLoading={loginMutation.isPending}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{settings?.registration_enabled && (
|
||||
<TabsContent value="register">
|
||||
<AuthForm
|
||||
mode="register"
|
||||
onSubmit={(data) => {
|
||||
registerMutation.mutate(data as InsertUser, {
|
||||
onError: (error) => {
|
||||
// We can't access form here directly easily without refactoring,
|
||||
// but we can pass a callback or handle it in AuthForm if we passed mutation there.
|
||||
// However, simpler is to catch it here if we want global toast.
|
||||
// The Requirements say "indicate failure".
|
||||
// To set FIELD errors, we must be inside the form submit context or have access to form methods.
|
||||
// Let's refactor AuthForm to handle the mutation itself or return the error?
|
||||
// Actually, simpler: pass the mutation TO AuthForm so it can handle onError.
|
||||
}
|
||||
})
|
||||
}}
|
||||
isLoading={registerMutation.isPending}
|
||||
registerMutation={registerMutation} // Pass mutation to handle errors inside
|
||||
/>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthForm({
|
||||
mode,
|
||||
onSubmit,
|
||||
isLoading,
|
||||
registerMutation,
|
||||
}: {
|
||||
mode: "login" | "register";
|
||||
onSubmit: (data: InsertUser | LoginUser) => void;
|
||||
isLoading: boolean;
|
||||
registerMutation?: any; // Type accurately if possible, but 'any' for quick fix avoids generic complexities
|
||||
}) {
|
||||
const { toast } = useToast();
|
||||
const form = useForm<InsertUser>({
|
||||
resolver: zodResolver(mode === "login" ? loginSchema : registerSchema),
|
||||
defaultValues: {
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (data: InsertUser | LoginUser) => {
|
||||
if (mode === 'register' && registerMutation) {
|
||||
registerMutation.mutate(data, {
|
||||
onError: (error: Error) => {
|
||||
const msg = error.message.toLowerCase();
|
||||
if (msg.includes("username")) {
|
||||
form.setError("username", { type: "manual", message: "Username already exists" });
|
||||
} else if (msg.includes("email")) {
|
||||
form.setError("email", { type: "manual", message: "Email already exists" });
|
||||
} else {
|
||||
toast({
|
||||
title: "Registration failed",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
onSubmit(data);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{mode === 'login' ? 'Username or Email' : 'Username'}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={mode === 'login' ? "Enter username or email" : "Choose a username"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{mode === "register" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" placeholder="Enter your email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button className="w-full" type="submit" disabled={isLoading}>
|
||||
{isLoading
|
||||
? mode === "login"
|
||||
? "Logging in..."
|
||||
: "Creating account..."
|
||||
: mode === "login"
|
||||
? "Sign In"
|
||||
: "Create Account"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user