diff --git a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf index 32099771d..0fd95f145 100644 --- a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf +++ b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf @@ -274,6 +274,18 @@ http { include proxy.conf; } + # Allow unauthenticated access to the first_time_login endpoint + # so the login page can load help text before authentication. + location /api/auth/first_time_login { + auth_request off; + limit_except GET { + deny all; + } + rewrite ^/api(/.*)$ $1 break; + proxy_pass http://frigate_api; + include proxy.conf; + } + location /api/stats { include auth_request.conf; access_log off; diff --git a/frigate/api/auth.py b/frigate/api/auth.py index 14fd804f7..1c1371f51 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -35,6 +35,23 @@ logger = logging.getLogger(__name__) router = APIRouter(tags=[Tags.auth]) +@router.get("/auth/first_time_login") +def first_time_login(request: Request): + """Return whether the admin first-time login help flag is set in config. + + This endpoint is intentionally unauthenticated so the login page can + query it before a user is authenticated. + """ + auth_config = request.app.frigate_config.auth + + return JSONResponse( + content={ + "admin_first_time_login": auth_config.admin_first_time_login + or auth_config.reset_admin_password + } + ) + + class RateLimiter: _limit = "" @@ -515,6 +532,11 @@ def login(request: Request, body: AppPostLoginBody): set_jwt_cookie( response, JWT_COOKIE_NAME, encoded_jwt, expiration, JWT_COOKIE_SECURE ) + # Clear admin_first_time_login flag after successful admin login so the + # UI stops showing the first-time login documentation link. + if role == "admin": + request.app.frigate_config.auth.admin_first_time_login = False + return response return JSONResponse(content={"message": "Login failed"}, status_code=401) diff --git a/frigate/app.py b/frigate/app.py index 858247866..30259ad3d 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -488,6 +488,8 @@ class FrigateApp: } ).execute() + self.config.auth.admin_first_time_login = True + logger.info("********************************************************") logger.info("********************************************************") logger.info("*** Auth is enabled, but no users exist. ***") diff --git a/frigate/config/auth.py b/frigate/config/auth.py index fd5d0e394..fced20620 100644 --- a/frigate/config/auth.py +++ b/frigate/config/auth.py @@ -38,6 +38,13 @@ class AuthConfig(FrigateBaseModel): default_factory=dict, title="Role to camera mappings. Empty list grants access to all cameras.", ) + admin_first_time_login: Optional[bool] = Field( + default=False, + title="Internal field to expose first-time admin login flag to the UI", + description=( + "When true the UI may show a help link on the login page informing users how to sign in after an admin password reset. " + ), + ) @field_validator("roles") @classmethod diff --git a/web/public/locales/en/components/auth.json b/web/public/locales/en/components/auth.json index 05c2a779f..56b750070 100644 --- a/web/public/locales/en/components/auth.json +++ b/web/public/locales/en/components/auth.json @@ -3,6 +3,7 @@ "user": "Username", "password": "Password", "login": "Login", + "firstTimeLogin": "Trying to log in for the first time? Credentials are printed in the Frigate logs.", "errors": { "usernameRequired": "Username is required", "passwordRequired": "Password is required", diff --git a/web/src/components/auth/AuthForm.tsx b/web/src/components/auth/AuthForm.tsx index 12e8f777e..8798b5d00 100644 --- a/web/src/components/auth/AuthForm.tsx +++ b/web/src/components/auth/AuthForm.tsx @@ -22,14 +22,24 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { AuthContext } from "@/context/auth-context"; import { useTranslation } from "react-i18next"; +import useSWR from "swr"; +import { LuExternalLink } from "react-icons/lu"; +import { useDocDomain } from "@/hooks/use-doc-domain"; +import { Card, CardContent } from "@/components/ui/card"; interface UserAuthFormProps extends React.HTMLAttributes {} export function UserAuthForm({ className, ...props }: UserAuthFormProps) { - const { t } = useTranslation(["components/auth"]); + const { t } = useTranslation(["components/auth", "common"]); + const { getLocaleDocUrl } = useDocDomain(); const [isLoading, setIsLoading] = React.useState(false); const { login } = React.useContext(AuthContext); + // need to use local fetcher because useSWR default fetcher is not set up in this context + const fetcher = (path: string) => axios.get(path).then((res) => res.data); + const { data } = useSWR("/auth/first_time_login", fetcher); + const showFirstTimeLink = data?.admin_first_time_login === true; + const formSchema = z.object({ user: z.string().min(1, t("form.errors.usernameRequired")), password: z.string().min(1, t("form.errors.passwordRequired")), @@ -136,6 +146,24 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) { + {showFirstTimeLink && ( + + +

+ {t("form.firstTimeLogin")} +

+ + {t("readTheDocumentation", { ns: "common" })} + + +
+
+ )} );