import { get, writable } from 'svelte/store';
import { goto } from '$app/navigation';
* The base URL of the chartered-web instance
*/
export const BASE_URL = import.meta.env.CHARTERED_WEB_URL || 'http://127.0.0.1:8888';
* Key in localStorage to store authentication information in
*/
const AUTH_LOCAL_STORAGE_KEY = 'auth.auth';
* Structure of the authentication information in local storage
*/
interface Auth {
uuid: string;
auth_key: string;
expires: number;
picture_url: string;
}
const initialState = window.localStorage.getItem(AUTH_LOCAL_STORAGE_KEY);
export const auth = writable<Auth | null>(initialState ? JSON.parse(initialState) : null);
auth.subscribe((v) => window.localStorage.setItem(AUTH_LOCAL_STORAGE_KEY, JSON.stringify(v)));
* Error response that all of our chartered-web endpoints conform to
*/
interface Error {
error?: string;
}
* Response type of /web/v1/auth/extend, used to extend the user's current session
*/
interface ExtendResponse {
expires: string;
}
type ExtendResult = ExtendResponse & Error;
* Attempt to extend the user's session, logging the user out if the session has already expired
* before we were called.
*/
export async function extendSession() {
const currentAuth = get(auth);
if (currentAuth === null) {
return;
} else if (currentAuth.expires < Date.now()) {
await logout();
return;
}
try {
const result = await fetch(`${BASE_URL}/a/${currentAuth.auth_key}/web/v1/auth/extend`);
const json: ExtendResult = await result.json();
if (json.error) {
throw new Error(json.error);
}
currentAuth.expires = Date.parse(json.expires);
auth.set(currentAuth);
} catch (e) {
if (e === 'Expired auth token') {
auth.set(null);
} else {
console.error('Failed to extend user session', e);
}
}
}
if (!(window as any).extendSessionInterval) {
extendSession();
(window as any).extendSessionInterval = setInterval(extendSession, 60000);
}
* Successful response type of /web/v1/auth/login/password, returned once the user has
* an active session on the backend
*/
interface LoginResponse {
user_uuid: string;
key: string;
expires: string;
picture_url: string;
}
type LoginResult = LoginResponse & Error;
* Attempt to log the user in using password-based auth with the given credentials,
* throwing an error if the credentials are invalid or another error occurred.
*
* @param username username to attempt to log in with
* @param password password to attempt to log in with
*/
export async function login(username: string, password: string) {
const result = await fetch(`${BASE_URL}/a/-/web/v1/auth/login/password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
const json: LoginResult = await result.json();
if (json.error) {
throw new Error(json.error);
}
auth.set({
auth_key: json.key,
expires: Date.parse(json.expires),
picture_url: json.picture_url,
uuid: json.user_uuid,
});
}
* Successful response type of /web/v1/auth/login/oauth/[provider]/begin, contains the URL
* the user needs to visit to complete the OAuth flow.
*/
interface LoginOAuthResponse {
redirect_url: string;
}
type LoginOAuthResult = LoginOAuthResponse & Error;
* Sends a GET request to the backend using the users current credentials.
*
* @param url url (without base) to send request to
*/
export async function request<T>(url: string): Promise<T> {
let token = get(auth)?.auth_key;
if (!token) {
throw new Error('Not authenticated');
}
let result = await fetch(`${BASE_URL}/a/${token}${url}`);
let json: T & Error = await result.json();
if (json.error) {
throw new Error(json.error);
}
return json;
}
* Grab an authentication URL for the provider and redirect the user to it.
*
* @param provider OAuth provider as configured on the backend to grab an auth link for
*/
export async function loginOAuth(provider: string) {
const result = await fetch(`${BASE_URL}/a/-/web/v1/auth/login/oauth/${provider}/begin`);
const json: LoginOAuthResult = await result.json();
if (json.error) {
throw new Error(json.error);
}
await goto(json.redirect_url);
}
* Send a request to the backend to clear this session, ignoring the response and clearing
* our local store regardless of the result.
*
* If the session still happens to be active on the backend after this, the user can still
* clear it out through the UI.
*/
export async function logout() {
try {
const authKey = get(auth)?.auth_key;
if (authKey) {
await fetch(`${BASE_URL}/a/${authKey}/web/v1/auth/logout`);
}
} catch (e) {
console.error('Failed to fully log user out of session', e);
} finally {
auth.set(null);
}
}
* A list of possible authentication methods for the user, returning OAuth providers and
* whether password auth is enabled.
*/
interface OAuthProviders {
password: boolean;
providers: string[];
}
* Grab all the possible authentication methods from the backend.
*/
export function fetchOAuthProviders(): Promise<OAuthProviders> {
return fetch(`${BASE_URL}/a/-/web/v1/auth/login/oauth/providers`).then((v) => v.json());
}
* Response of /web/v1/auth/register/password endpoint for password-based authentication.
*/
interface RegisterResponse {
success: boolean;
}
type RegisterResult = RegisterResponse & Error;
* Attempt to register a user with the given credentials, throwing an error if registration
* fails for whatever reason.
*
* @param username username to register
* @param password password to register
*/
export async function register(username: string, password: string) {
let result = await fetch(`${BASE_URL}/a/-/web/v1/auth/register/password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
let json: RegisterResult = await result.json();
if (json.error) {
throw new Error(json.error);
} else if (!json.success) {
throw new Error('Failed to register, please try again later.');
}
}