// Response types // ============== import { user } from "./UserStore" export type GenericResponse = { value: string, message: string, errors?: GenericResponse[], extra?: { [index: string]: Object }, } export type User = { id: string, username: string, email: string, otp_enabled: boolean, subscription: Subscription, storage_space_used: number, filesystem_storage_used: number, filesystem_node_count: number, is_admin: boolean, balance_micro_eur: number, subscription_months: number, subscription_period_end: string, monthly_transfer_cap: number, monthly_transfer_used: number, file_viewer_branding: Map, embed_domains: string, affiliate_user_name: string, checkout_country: string, checkout_name: string, checkout_provider: string, usage_based_billing: boolean, } export type Subscription = { id: string, name: string, type: string, monthly_fee: number, min_months: number, file_size_limit: number, storage_limit: number, price_per_tb_storage: number, price_per_tb_bandwidth: number, monthly_transfer_cap: number, file_sharing: boolean, } // Utility funcs // ============= export const get_endpoint = () => { if ((window as any).api_endpoint !== undefined) { return (window as any).api_endpoint as string } console.warn("api_endpoint property is not defined on window") return "/api" } export const get_hostname = () => { if ((window as any).server_hostname !== undefined) { return (window as any).server_hostname as string } console.warn("server_hostname property is not defined on window") return "undefined" } export const check_response = async (resp: Response) => { let text = await resp.text() if (resp.status >= 400) { let error: any try { error = JSON.parse(text) as GenericResponse } catch (err) { error = text } throw error } return JSON.parse(text) } export const dict_to_form = (dict: Object) => { let form = new FormData() for (let key of Object.keys(dict)) { if (dict[key] === undefined) { continue } else if (dict[key] instanceof Date) { form.append(key, new Date(dict[key]).toISOString()) } else if (typeof dict[key] === "object") { form.append(key, JSON.stringify(dict[key])) } else { form.append(key, dict[key]) } } return form } // API methods // =========== export type UserSession = { auth_key: string, creation_ip_address: string, user_agent: string, app_name: string, creation_time: string, last_used_time: string, valid_domains: string[], } // If cached_user is undefined it means that the value is not initialized yet, // null means the user is not logged in let cached_user: User = undefined // The request which is currently in flight. Callers which arrive while the // request is still running share this promise, so the user is only requested // from the server once let user_request: Promise | null = null export const get_user = async (): Promise => { if (cached_user !== undefined) { return cached_user } if (user_request === null) { user_request = request_user() } const u = await user_request if (u === undefined) { // The request failed. Clear it so the next caller tries again user_request = null } return u } const request_user = async (): Promise => { try { let resp = await fetch(get_endpoint() + "/user") if (resp.status === 401) { // User is not authenticated cached_user = null } else if (resp.status >= 400) { throw await resp.text() } else { cached_user = (await resp.json()) as User } } catch (err) { alert("failed to fetch user:\n" + err) } user.set(cached_user) return cached_user } export const put_user = async (data: Object) => { await check_response(await fetch( get_endpoint() + "/user", { method: "PUT", body: dict_to_form(data) }, )) // Fetch the user again to update the store. The submitted fields don't // always match the fields on the user object, and the server can change or // ignore them, so we can't apply the submitted values locally await request_user() } export const logout_user = async (redirect_path: string) => { await check_response(await fetch( get_endpoint() + "/user/session", { method: "DELETE" }, )) window.location.pathname = redirect_path } export type VATRate = { name: string, vat: number, alpha2: string, alpha3: string, } export const get_misc_vat_rate = async (country_code: string) => { return await check_response(await fetch(get_endpoint() + "/misc/vat_rate/" + country_code)) as VATRate } // The available subscription plans. The discount for a longer subscription // period is applied by the server too, see SubscriptionDiscount in Go export type SubscriptionPeriod = { months: number, discount: number, name: string, label: string, } export const subscription_periods: SubscriptionPeriod[] = [ { months: 1, discount: 1, name: "Monthly", label: "Monthly" }, { months: 6, discount: 0.9, name: "Bi-yearly", label: "Bi-yearly (save 10%)" }, { months: 12, discount: 0.8, name: "Yearly", label: "Yearly (save 20%)" }, ] // Subscriptions are charged per day. This is the average length of a month, the // same value the server divides the monthly fee by export const days_per_month = 365.25 / 12 export const get_misc_subscriptions = async () => { return await check_response( await fetch(get_endpoint() + "/misc/subscriptions") ) as Subscription[] } export type Invoice = { id: string, time: Date, amount: number, vat: number, country: string, payment_gateway: string, payment_method: string, status: "open" | "processing" | "paid" | "expired" | "canceled", } export const get_user_invoices = async (): Promise => { const invoices = await check_response( await fetch(get_endpoint() + "/user/invoice"), ) as Invoice[] invoices.forEach(row => row.time = new Date(row.time)) invoices.sort((a, b) => b.time.getTime() - a.time.getTime()) return invoices }