@@ -11,7 +11,7 @@ import { useLocation, useNavigate } from "react-router";
1111import ThemeSwitch from "./ThemeSwitch" ;
1212
1313export function Login ( ) {
14- const { loginBasic, loading, error, setAuth, setUser } = useAuth ( ) ;
14+ const { loginBasic, loading, error, setAuth, setUser, authFetch } = useAuth ( ) ;
1515 const navigate = useNavigate ( ) ;
1616 const location = useLocation ( ) ;
1717
@@ -20,18 +20,72 @@ export function Login() {
2020 const [ password , setPassword ] = useState ( "" ) ;
2121 const [ useSso , setUseSso ] = useState ( false ) ;
2222
23- // Refs for focus trap
2423 const serverRef = useRef < HTMLInputElement | null > ( null ) ;
2524 const userRef = useRef < HTMLInputElement | null > ( null ) ;
2625 const passRef = useRef < HTMLInputElement | null > ( null ) ;
2726 const submitRef = useRef < HTMLButtonElement | null > ( null ) ;
2827
28+ // Ref for next token refresh; if needed, can alternatively export/set via context
29+ const nextTokenRefreshRef = useRef < Date | null > ( null ) ;
30+
2931 useEffect ( ( ) => {
30- // Auto-focus server field on mount
3132 serverRef . current ?. focus ( ) ;
3233 } , [ ] ) ;
3334
34- // Effect to check query params for tokens and log in if found
35+ const normalizeServer = useCallback ( ( raw : string ) => {
36+ if ( ! raw ) return raw ;
37+ let s = raw . trim ( ) ;
38+ if ( ! / ^ h t t p s ? : \/ \/ / i. test ( s ) ) s = `https://${ s } ` ;
39+ s = s . replace ( / \/ + $ / , "" ) ;
40+ return s ;
41+ } , [ ] ) ;
42+
43+ // Helper to compute next token refresh (same as in AuthContext)
44+ function base64UrlDecode ( segment : string ) : string {
45+ try {
46+ let s = segment . replace ( / - / g, "+" ) . replace ( / _ / g, "/" ) ;
47+ const pad = s . length % 4 ;
48+ if ( pad ) s += "=" . repeat ( 4 - pad ) ;
49+ if ( typeof atob === "function" ) return atob ( s ) ;
50+ if ( typeof window === "undefined" ) return "" ;
51+ // @ts -ignore
52+ return atob ( s ) ;
53+ } catch {
54+ return "" ;
55+ }
56+ }
57+ function parseJwt ( token : string ) {
58+ const parts = token . split ( "." ) ;
59+ if ( parts . length !== 3 ) return null ;
60+ try {
61+ const json = base64UrlDecode ( parts [ 1 ] ) ;
62+ return JSON . parse ( json ) ;
63+ } catch {
64+ return null ;
65+ }
66+ }
67+ function computeNextTokenRefresh ( token : string ) : Date {
68+ const payload = parseJwt ( token ) ;
69+ if ( ! payload ) return new Date ( ) ;
70+ const exp = payload . Exp ?? payload . exp ;
71+ const creation =
72+ payload . Creation ?? payload . creation ?? payload . iat ?? payload . Iat ;
73+ const now = Date . now ( ) ;
74+ if (
75+ typeof exp === "number" &&
76+ typeof creation === "number" &&
77+ exp > creation
78+ ) {
79+ const lifetimeSec = exp - creation ;
80+ return new Date ( now + lifetimeSec * 1000 ) ;
81+ }
82+ if ( typeof exp === "number" ) {
83+ return new Date ( exp * 1000 ) ;
84+ }
85+ return new Date ( ) ;
86+ }
87+
88+ // Effect to handle login via tokens in URL query params
3589 useEffect ( ( ) => {
3690 const params = new URLSearchParams ( location . search ) ;
3791 const accessToken = params . get ( "access_token" ) ;
@@ -43,46 +97,40 @@ export function Login() {
4397 const normalizedServer =
4498 normalizeServer ( server ) || window . location . origin ;
4599
46- // Set tokens in auth context and localStorage
100+ setServer ( normalizedServer ) ;
101+ localStorage . setItem ( "app_server_url" , normalizedServer ) ;
102+
47103 setAuth ( { access_token : accessToken , refresh_token : refreshToken } ) ;
48104 localStorage . setItem ( "app_refresh_token" , refreshToken ) ;
49105
50- // Fetch current user
51- const meResponse = await fetch ( `${ normalizedServer } /api/users/me` , {
52- headers : {
53- Authorization : `Bearer ${ accessToken } ` ,
54- Accept : "application/json" ,
55- } ,
56- } ) ;
57-
58- if ( ! meResponse . ok ) {
59- throw new Error ( "Failed to fetch user data" ) ;
60- }
106+ nextTokenRefreshRef . current = computeNextTokenRefresh ( accessToken ) ;
61107
108+ // Use authFetch from context to support token refresh etc.
109+ const meResponse = await authFetch (
110+ `${ normalizedServer } /api/users/me` ,
111+ ) ;
112+ if ( ! meResponse . ok ) throw new Error ( "Failed to fetch user" ) ;
62113 const me = await meResponse . json ( ) ;
63114 setUser ( me ) ;
64115
65- // Redirect to library page
66116 navigate ( "/library" , { replace : true } ) ;
67117
68- // Clear query params from URL to prevent reuse
118+ // Clear query parameters from URL
69119 window . history . replaceState ( { } , document . title , location . pathname ) ;
70- } catch ( err ) {
71- // Could optionally handle errors here
120+ } catch {
121+ // Optionally handle error: e.g. clearing tokens, showing message
72122 }
73123 } ) ( ) ;
74124 }
75- } , [ location . search , navigate , server , setAuth , setUser ] ) ;
76-
77- const normalizeServer = useCallback ( ( raw : string ) => {
78- if ( ! raw ) return raw ;
79- let s = raw . trim ( ) ;
80- if ( ! / ^ h t t p s ? : \/ \/ / i. test ( s ) ) {
81- s = `https://${ s } ` ;
82- }
83- s = s . replace ( / \/ + $ / , "" ) ;
84- return s ;
85- } , [ ] ) ;
125+ } , [
126+ location . search ,
127+ navigate ,
128+ server ,
129+ setAuth ,
130+ setUser ,
131+ authFetch ,
132+ normalizeServer ,
133+ ] ) ;
86134
87135 const onSubmit = async ( e : FormEvent ) => {
88136 e . preventDefault ( ) ;
@@ -108,10 +156,7 @@ export function Login() {
108156 submitRef . current ,
109157 ] . filter (
110158 ( el ) : el is HTMLInputElement | HTMLButtonElement =>
111- ! ! el &&
112- ( typeof ( el as any ) . disabled === "boolean"
113- ? ! ( el as any ) . disabled
114- : true ) ,
159+ ! ! el && ( ! ( el as any ) . disabled ?? true ) ,
115160 ) ;
116161 if ( elems . length === 0 ) return ;
117162 const active = document . activeElement as HTMLElement | null ;
0 commit comments