File size: 2,974 Bytes
00a4363
1e1198f
00a4363
1e1198f
070c931
00a4363
 
 
 
1e1198f
00a4363
 
 
 
 
 
1e1198f
 
 
00a4363
 
 
1e1198f
 
00a4363
 
 
 
 
 
 
 
 
 
1e1198f
 
 
 
 
 
 
 
 
 
 
00a4363
 
070c931
00a4363
 
 
 
1e1198f
 
 
 
 
00a4363
1e1198f
 
 
 
00a4363
1e1198f
 
00a4363
1e1198f
00a4363
1e1198f
00a4363
1e1198f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
00a4363
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
import { useNavigate } from "react-router-dom";
import { authApi, type LoginResult, type User } from "../api/auth";
import { SESSION_EXPIRED_EVENT, tokens } from "../api/client";
import { clearSourceCache } from "../components/video/sourceCache";

type AuthState = {
  user: User | null;
  loading: boolean;
  signingOut: boolean;
  complete: (result: LoginResult) => void;
  logout: () => Promise<void>;
};

const AuthContext = createContext<AuthState | null>(null);

/** How long sign out waits for the server to revoke the session before moving on. */
const REVOKE_TIMEOUT_MS = 5000;

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(Boolean(tokens.access));
  const [signingOut, setSigningOut] = useState(false);
  const navigate = useNavigate();

  useEffect(() => {
    if (!tokens.access) return;
    authApi
      .me()
      .then(setUser)
      .catch(() => tokens.clear())
      .finally(() => setLoading(false));
  }, []);

  useEffect(() => {
    const expire = () => {
      tokens.clear();
      clearSourceCache();
      setUser(null);
      navigate("/", { replace: true });
    };
    window.addEventListener(SESSION_EXPIRED_EVENT, expire);
    return () => window.removeEventListener(SESSION_EXPIRED_EVENT, expire);
  }, [navigate]);

  const complete = useCallback((result: LoginResult) => {
    tokens.set(result.access_token, result.refresh_token);
    clearSourceCache();
    setUser(result.user);
  }, []);

  const logout = useCallback(async () => {
    setSigningOut(true);
    // Drop the session locally first so nothing (in this tab or another) can use it again.
    const access = tokens.access;
    tokens.clear();
    clearSourceCache();
    try {
      if (access) {
        const timeout = new Promise<void>((resolve) => setTimeout(resolve, REVOKE_TIMEOUT_MS));
        await Promise.race([authApi.logout(access).catch(() => undefined), timeout]);
      }
    } finally {
      // Navigate in the same batch as clearing the user so AppShell never sees a signed out render.
      navigate("/", { replace: true });
      setUser(null);
      setSigningOut(false);
    }
  }, [navigate]);

  const value = useMemo(
    () => ({ user, loading, signingOut, complete, logout }),
    [user, loading, signingOut, complete, logout],
  );
  return (
    <AuthContext.Provider value={value}>
      {children}
      {signingOut && (
        <div className="boot signout-overlay" role="status" aria-label="Signing out">
          <div className="orbit-loader">
            <span />
          </div>
        </div>
      )}
    </AuthContext.Provider>
  );
}

export function useAuth(): AuthState {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be used inside AuthProvider");
  return ctx;
}