Files
fnx_web/svelte/src/lib/NovaAPI.ts

251 lines
6.6 KiB
TypeScript

// 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<string, string>,
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 = () => window.api_endpoint
export const get_hostname = () => window.server_hostname
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[],
// Whether this is the key the list of sessions was requested with
current: boolean,
// What the key is allowed to do
filesystem_read: boolean,
filesystem_write: boolean,
account_read: boolean,
account_write: boolean,
session_read: boolean,
session_write: boolean,
billing_read: boolean,
billing_write: boolean,
// IDs of the directories the key is limited to. Empty means no limit
filesystem_dirs: string[],
}
// If cached_user is undefined it means that the value is not initialized yet,
// null means the user is not logged in. Some pages have it embedded by the server
let cached_user: User = window.user
// 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<User> | null = null
export const get_user = async (): Promise<User> => {
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<User> => {
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
// The price of the subscription period the user is in. This is also how far
// the balance can go below zero before usage above the plan is refused
export const period_price = (u: User) => {
const period = subscription_periods.find(p => p.months === u.subscription_months)
return period === undefined ? 0 :
u.subscription.monthly_fee * period.months * period.discount
}
// Whether usage above a limit of the plan is billed instead of refused. price
// is what the plan charges for that usage. The server does the same check in
// billsExtra
export const bills_extra = (u: User, price: number) =>
u.usage_based_billing && price > 0 && u.balance_micro_eur >= -period_price(u)
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<Invoice[]> => {
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
}