Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions surfsense_web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,6 @@ next-env.d.ts

# source
/.source/

.pnpm-store/

13 changes: 8 additions & 5 deletions surfsense_web/app/(home)/login/GoogleLoginButton.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"use client";
import { IconBrandGoogleFilled } from "@tabler/icons-react";
import { motion } from "motion/react";
import { useTranslations } from "next-intl";
import { Logo } from "@/components/Logo";
import { AmbientBackground } from "./AmbientBackground";

export function GoogleLoginButton() {
const t = useTranslations('auth');

const handleGoogleLogin = () => {
// Redirect to Google OAuth authorization URL
fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/google/authorize`)
Expand All @@ -31,7 +34,7 @@ export function GoogleLoginButton() {
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center">
<Logo className="rounded-md" />
<h1 className="my-8 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:text-4xl">
Welcome Back
{t('welcome_back')}
</h1>

<motion.div
Expand Down Expand Up @@ -65,14 +68,14 @@ export function GoogleLoginButton() {
</svg>
<div className="ml-1">
<p className="text-sm font-medium">
SurfSense Cloud is currently in development. Check{" "}
{t('cloud_dev_notice')}{" "}
<a
href="/docs"
className="text-blue-600 underline dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
>
Docs
{t('docs')}
</a>{" "}
for more information on Self-Hosted version.
{t('cloud_dev_self_hosted')}
</p>
</div>
</motion.div>
Expand All @@ -91,7 +94,7 @@ export function GoogleLoginButton() {
<div className="absolute -bottom-px -right-px h-4 w-4 rounded-br-lg border-b-2 border-r-2 border-blue-500 bg-transparent transition-all duration-200 group-hover/btn:-bottom-2 group-hover/btn:-right-2"></div>
</div>
<IconBrandGoogleFilled className="h-5 w-5 text-neutral-700 dark:text-neutral-200" />
<span className="text-base font-medium">Continue with Google</span>
<span className="text-base font-medium">{t('continue_with_google')}</span>
</motion.button>
</div>
</div>
Expand Down
141 changes: 72 additions & 69 deletions surfsense_web/app/(home)/login/LocalLoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { useTranslations } from "next-intl";
import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors";

export function LocalLoginForm() {
const t = useTranslations('auth');
const tCommon = useTranslations('common');
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
Expand All @@ -29,7 +32,7 @@ export function LocalLoginForm() {
setErrorTitle(null);

// Show loading toast
const loadingToast = toast.loading("Signing you in...");
const loadingToast = toast.loading(tCommon('loading'));

try {
// Create form data for the API request
Expand All @@ -56,7 +59,7 @@ export function LocalLoginForm() {
}

// Success toast
toast.success("Login successful!", {
toast.success(t('login_success'), {
id: loadingToast,
description: "Redirecting to dashboard...",
duration: 2000,
Expand Down Expand Up @@ -167,84 +170,84 @@ export function LocalLoginForm() {
</div>
</motion.div>
)}
</AnimatePresence>
</AnimatePresence>

<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Email
</label>
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{t('email')}
</label>
<input
id="email"
type="email"
required
value={username}
onChange={(e) => setUsername(e.target.value)}
className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${
error
? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700"
}`}
disabled={isLoading}
/>
</div>

<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{t('password')}
</label>
<div className="relative">
<input
id="email"
type="email"
id="password"
type={showPassword ? "text" : "password"}
required
value={username}
onChange={(e) => setUsername(e.target.value)}
className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${
value={password}
onChange={(e) => setPassword(e.target.value)}
className={`mt-1 block w-full rounded-md border pr-10 px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${
error
? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700"
}`}
disabled={isLoading}
/>
</div>

<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
<button
type="button"
onClick={() => setShowPassword((prev) => !prev)}
className="absolute inset-y-0 right-0 flex items-center pr-3 mt-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
aria-label={showPassword ? t('hide_password') : t('show_password')}
>
Password
</label>
<div className="relative">
<input
id="password"
type={showPassword ? "text" : "password"}
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className={`mt-1 block w-full rounded-md border pr-10 px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${
error
? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700"
}`}
disabled={isLoading}
/>
<button
type="button"
onClick={() => setShowPassword((prev) => !prev)}
className="absolute inset-y-0 right-0 flex items-center pr-3 mt-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
aria-label={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>

<button
type="submit"
disabled={isLoading}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"
>
{isLoading ? "Signing in..." : "Sign in"}
</button>
</form>

{authType === "LOCAL" && (
<div className="mt-4 text-center text-sm">
<p className="text-gray-600 dark:text-gray-400">
Don&apos;t have an account?{" "}
<Link
href="/register"
className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400"
>
Register here
</Link>
</p>
</div>
)}
</div>

<button
type="submit"
disabled={isLoading}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"
>
{isLoading ? tCommon('loading') : t('sign_in')}
</button>
</form>

{authType === "LOCAL" && (
<div className="mt-4 text-center text-sm">
<p className="text-gray-600 dark:text-gray-400">
{t('dont_have_account')}{" "}
<Link
href="/register"
className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400"
>
{t('sign_up')}
</Link>
</p>
</div>
)}
</div>
);
}
13 changes: 8 additions & 5 deletions surfsense_web/app/(home)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import { AnimatePresence, motion } from "motion/react";
import { useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { toast } from "sonner";
import { useTranslations } from "next-intl";
import { Logo } from "@/components/Logo";
import { getAuthErrorDetails, shouldRetry } from "@/lib/auth-errors";
import { AmbientBackground } from "./AmbientBackground";
import { GoogleLoginButton } from "./GoogleLoginButton";
import { LocalLoginForm } from "./LocalLoginForm";

function LoginContent() {
const t = useTranslations('auth');
const tCommon = useTranslations('common');
const [authType, setAuthType] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [urlError, setUrlError] = useState<{ title: string; message: string } | null>(null);
Expand All @@ -26,15 +29,15 @@ function LoginContent() {

// Show registration success message
if (registered === "true") {
toast.success("Registration successful!", {
description: "You can now sign in with your credentials",
toast.success(t('register_success'), {
description: t('login_subtitle'),
duration: 5000,
});
}

// Show logout confirmation
if (logout === "true") {
toast.success("Logged out successfully", {
toast.success(tCommon('success'), {
description: "You have been securely logged out",
Comment on lines +40 to 41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Translate the hardcoded logout message.

Line 41 contains a hardcoded string "You have been securely logged out" that should be translated for consistency with the rest of the i18n implementation.

Apply this diff:

     if (logout === "true") {
       toast.success(tCommon('success'), {
-        description: "You have been securely logged out",
+        description: t('logout_description'),
         duration: 3000,
       });
     }

Then add "logout_description": "You have been securely logged out" to the auth namespace in your message files.

🤖 Prompt for AI Agents
In surfsense_web/app/(home)/login/page.tsx around lines 40 to 41 replace the
hardcoded toast description string with the i18n lookup (e.g.
tAuth('logout_description')) so the logout message is translated; then add
"logout_description": "You have been securely logged out" to the auth namespace
in the message files for each locale.

duration: 3000,
});
Expand Down Expand Up @@ -93,7 +96,7 @@ function LoginContent() {
<Logo className="rounded-md" />
<div className="mt-8 flex items-center space-x-2">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
<span className="text-muted-foreground">Loading...</span>
<span className="text-muted-foreground">{tCommon('loading')}</span>
</div>
</div>
</div>
Expand All @@ -110,7 +113,7 @@ function LoginContent() {
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center">
<Logo className="rounded-md" />
<h1 className="my-8 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:text-4xl">
Sign In
{t('sign_in')}
</h1>

{/* URL Error Display */}
Expand Down
Loading
Loading