mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-24 22:04:39 +00:00
31381f67f4
## Description: Fixes #952 Enabled @typescript-eslint/prefer-nullish-coalescing rule and worked through every error, introducing ?? and ??= operators or disabling errors with inline comments where appropriate, to the best of my ability. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: g_santos_m
250 lines
6.4 KiB
TypeScript
250 lines
6.4 KiB
TypeScript
import { decodeJwt } from "jose";
|
|
import { z } from "zod/v4";
|
|
import {
|
|
RefreshResponseSchema,
|
|
TokenPayload,
|
|
TokenPayloadSchema,
|
|
UserMeResponse,
|
|
UserMeResponseSchema,
|
|
} from "../core/ApiSchemas";
|
|
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
|
|
|
function getAudience() {
|
|
const { hostname } = new URL(window.location.href);
|
|
const domainname = hostname.split(".").slice(-2).join(".");
|
|
return domainname;
|
|
}
|
|
|
|
export function getApiBase() {
|
|
const domainname = getAudience();
|
|
return domainname === "localhost"
|
|
? (localStorage.getItem("apiHost") ?? "http://localhost:8787")
|
|
: `https://api.${domainname}`;
|
|
}
|
|
|
|
function getToken(): string | null {
|
|
// Check window hash
|
|
const { hash } = window.location;
|
|
if (hash.startsWith("#")) {
|
|
const params = new URLSearchParams(hash.slice(1));
|
|
const token = params.get("token");
|
|
if (token) {
|
|
localStorage.setItem("token", token);
|
|
params.delete("token");
|
|
params.toString();
|
|
}
|
|
// Clean the URL
|
|
history.replaceState(
|
|
null,
|
|
"",
|
|
window.location.pathname +
|
|
window.location.search +
|
|
(params.size > 0 ? "#" + params.toString() : ""),
|
|
);
|
|
}
|
|
|
|
// Check cookie
|
|
const cookie = document.cookie
|
|
.split(";")
|
|
.find((c) => c.trim().startsWith("token="))
|
|
?.trim()
|
|
.substring(6);
|
|
if (cookie !== undefined) {
|
|
return cookie;
|
|
}
|
|
|
|
// Check local storage
|
|
return localStorage.getItem("token");
|
|
}
|
|
|
|
async function clearToken() {
|
|
localStorage.removeItem("token");
|
|
__isLoggedIn = false;
|
|
const config = await getServerConfigFromClient();
|
|
const audience = config.jwtAudience();
|
|
const isSecure = window.location.protocol === "https:";
|
|
const secure = isSecure ? "; Secure" : "";
|
|
document.cookie = `token=logged_out; Path=/; Max-Age=0; Domain=${audience}${secure}`;
|
|
}
|
|
|
|
export function discordLogin() {
|
|
window.location.href = `${getApiBase()}/login/discord?redirect_uri=${window.location.href}`;
|
|
}
|
|
|
|
export function getAuthHeader(): string {
|
|
const token = getToken();
|
|
if (!token) return "";
|
|
return `Bearer ${token}`;
|
|
}
|
|
|
|
export async function logOut(allSessions: boolean = false) {
|
|
const token = getToken();
|
|
if (token === null) return;
|
|
clearToken();
|
|
|
|
const response = await fetch(
|
|
getApiBase() + (allSessions ? "/revoke" : "/logout"),
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
authorization: `Bearer ${token}`,
|
|
},
|
|
},
|
|
);
|
|
|
|
if (response.ok === false) {
|
|
console.error("Logout failed", response);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export type IsLoggedInResponse =
|
|
| { token: string; claims: TokenPayload }
|
|
| false;
|
|
let __isLoggedIn: IsLoggedInResponse | undefined = undefined;
|
|
export function isLoggedIn(): IsLoggedInResponse {
|
|
__isLoggedIn ??= _isLoggedIn();
|
|
|
|
return __isLoggedIn;
|
|
}
|
|
function _isLoggedIn(): IsLoggedInResponse {
|
|
try {
|
|
const token = getToken();
|
|
if (!token) {
|
|
// console.log("No token found");
|
|
return false;
|
|
}
|
|
|
|
// Verify the JWT (requires browser support)
|
|
// const jwks = createRemoteJWKSet(
|
|
// new URL(getApiBase() + "/.well-known/jwks.json"),
|
|
// );
|
|
// const { payload, protectedHeader } = await jwtVerify(token, jwks, {
|
|
// issuer: getApiBase(),
|
|
// audience: getAudience(),
|
|
// });
|
|
|
|
// Decode the JWT
|
|
const payload = decodeJwt(token);
|
|
const { iss, aud, exp, iat } = payload;
|
|
|
|
if (iss !== getApiBase()) {
|
|
// JWT was not issued by the correct server
|
|
console.error(
|
|
'unexpected "iss" claim value',
|
|
// JSON.stringify(payload, null, 2),
|
|
);
|
|
logOut();
|
|
return false;
|
|
}
|
|
if (aud !== getAudience()) {
|
|
// JWT was not issued for this website
|
|
console.error(
|
|
'unexpected "aud" claim value',
|
|
// JSON.stringify(payload, null, 2),
|
|
);
|
|
logOut();
|
|
return false;
|
|
}
|
|
const now = Math.floor(Date.now() / 1000);
|
|
if (exp !== undefined && now >= exp) {
|
|
// JWT expired
|
|
console.error(
|
|
'after "exp" claim value',
|
|
// JSON.stringify(payload, null, 2),
|
|
);
|
|
logOut();
|
|
return false;
|
|
}
|
|
const refreshAge: number = 3 * 24 * 3600; // 3 days
|
|
if (iat !== undefined && now >= iat + refreshAge) {
|
|
console.log("Refreshing access token...");
|
|
postRefresh().then((success) => {
|
|
if (success) {
|
|
console.log("Refreshed access token successfully.");
|
|
} else {
|
|
console.error("Failed to refresh access token.");
|
|
// TODO: Update the UI to show logged out state
|
|
}
|
|
});
|
|
}
|
|
|
|
const result = TokenPayloadSchema.safeParse(payload);
|
|
if (!result.success) {
|
|
const error = z.prettifyError(result.error);
|
|
// Invalid response
|
|
console.error("Invalid payload", error);
|
|
return false;
|
|
}
|
|
|
|
const claims = result.data;
|
|
return { token, claims };
|
|
} catch (e) {
|
|
console.log(e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function postRefresh(): Promise<boolean> {
|
|
try {
|
|
const token = getToken();
|
|
if (!token) return false;
|
|
|
|
// Refresh the JWT
|
|
const response = await fetch(getApiBase() + "/refresh", {
|
|
method: "POST",
|
|
headers: {
|
|
authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
if (response.status === 401) {
|
|
clearToken();
|
|
return false;
|
|
}
|
|
if (response.status !== 200) return false;
|
|
const body = await response.json();
|
|
const result = RefreshResponseSchema.safeParse(body);
|
|
if (!result.success) {
|
|
const error = z.prettifyError(result.error);
|
|
console.error("Invalid response", error);
|
|
return false;
|
|
}
|
|
localStorage.setItem("token", result.data.token);
|
|
return true;
|
|
} catch (e) {
|
|
__isLoggedIn = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function getUserMe(): Promise<UserMeResponse | false> {
|
|
try {
|
|
const token = getToken();
|
|
if (!token) return false;
|
|
|
|
// Get the user object
|
|
const response = await fetch(getApiBase() + "/users/@me", {
|
|
headers: {
|
|
authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
if (response.status === 401) {
|
|
clearToken();
|
|
return false;
|
|
}
|
|
if (response.status !== 200) return false;
|
|
const body = await response.json();
|
|
const result = UserMeResponseSchema.safeParse(body);
|
|
if (!result.success) {
|
|
const error = z.prettifyError(result.error);
|
|
console.error("Invalid response", error);
|
|
return false;
|
|
}
|
|
return result.data;
|
|
} catch (e) {
|
|
__isLoggedIn = false;
|
|
return false;
|
|
}
|
|
}
|