2025-03-21 12:57:53 +01:00
|
|
|
// Response types
|
|
|
|
|
// ==============
|
|
|
|
|
|
2026-07-30 17:07:46 +02:00
|
|
|
import { user } from "./UserStore"
|
|
|
|
|
|
2025-03-21 12:57:53 +01:00
|
|
|
export type GenericResponse = {
|
|
|
|
|
value: string,
|
|
|
|
|
message: string,
|
2025-03-25 17:58:26 +01:00
|
|
|
errors?: GenericResponse[],
|
|
|
|
|
extra?: { [index: string]: Object },
|
2025-03-21 12:57:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type User = {
|
2026-08-03 23:31:43 +02:00
|
|
|
id: string,
|
2025-03-21 12:57:53 +01:00
|
|
|
username: string,
|
|
|
|
|
email: string,
|
2025-03-25 17:58:26 +01:00
|
|
|
otp_enabled: boolean,
|
2025-03-21 12:57:53 +01:00
|
|
|
subscription: Subscription,
|
|
|
|
|
storage_space_used: number,
|
|
|
|
|
filesystem_storage_used: number,
|
2026-01-21 23:48:34 +01:00
|
|
|
filesystem_node_count: number,
|
2025-03-21 12:57:53 +01:00
|
|
|
is_admin: boolean,
|
|
|
|
|
balance_micro_eur: number,
|
2026-09-30 19:26:21 +02:00
|
|
|
subscription_months: number,
|
|
|
|
|
subscription_period_end: string,
|
2025-03-21 12:57:53 +01:00
|
|
|
monthly_transfer_cap: number,
|
|
|
|
|
monthly_transfer_used: number,
|
|
|
|
|
file_viewer_branding: Map<string, string>,
|
2026-08-03 23:31:43 +02:00
|
|
|
embed_domains: string,
|
2025-03-21 12:57:53 +01:00
|
|
|
affiliate_user_name: string,
|
2025-05-15 17:00:20 +02:00
|
|
|
checkout_country: string,
|
|
|
|
|
checkout_name: string,
|
|
|
|
|
checkout_provider: string,
|
2026-09-30 19:26:21 +02:00
|
|
|
usage_based_billing: boolean,
|
2025-03-21 12:57:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type Subscription = {
|
|
|
|
|
id: string,
|
|
|
|
|
name: string,
|
|
|
|
|
type: string,
|
2026-09-30 19:26:21 +02:00
|
|
|
monthly_fee: number,
|
|
|
|
|
min_months: number,
|
2025-03-21 12:57:53 +01:00
|
|
|
file_size_limit: number,
|
2026-07-30 17:07:46 +02:00
|
|
|
storage_limit: number,
|
2025-03-21 12:57:53 +01:00
|
|
|
price_per_tb_storage: number,
|
|
|
|
|
price_per_tb_bandwidth: number,
|
|
|
|
|
monthly_transfer_cap: number,
|
2026-07-30 17:07:46 +02:00
|
|
|
file_sharing: boolean,
|
2025-03-21 12:57:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Utility funcs
|
|
|
|
|
// =============
|
|
|
|
|
|
2026-10-01 02:06:00 +02:00
|
|
|
export const get_endpoint = () => window.api_endpoint
|
|
|
|
|
export const get_hostname = () => window.server_hostname
|
2025-03-28 14:16:20 +01:00
|
|
|
|
2025-03-21 12:57:53 +01:00
|
|
|
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
|
|
|
|
|
// ===========
|
|
|
|
|
|
2025-07-30 18:53:10 +02:00
|
|
|
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[],
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 17:07:46 +02:00
|
|
|
// If cached_user is undefined it means that the value is not initialized yet,
|
2026-10-01 02:06:00 +02:00
|
|
|
// null means the user is not logged in. Some pages have it embedded by the server
|
|
|
|
|
let cached_user: User = window.user
|
2026-07-30 17:07:46 +02:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
2025-10-09 15:48:23 +02:00
|
|
|
export const get_user = async (): Promise<User> => {
|
2026-07-30 17:07:46 +02:00
|
|
|
if (cached_user !== undefined) {
|
2025-10-09 15:48:23 +02:00
|
|
|
return cached_user
|
2025-03-21 12:57:53 +01:00
|
|
|
}
|
2026-07-30 17:07:46 +02:00
|
|
|
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
|
|
|
|
|
}
|
2025-03-21 12:57:53 +01:00
|
|
|
|
2026-07-30 17:07:46 +02:00
|
|
|
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)
|
|
|
|
|
}
|
2025-03-21 12:57:53 +01:00
|
|
|
|
2026-07-30 17:07:46 +02:00
|
|
|
user.set(cached_user)
|
2025-10-09 15:48:23 +02:00
|
|
|
return cached_user
|
2025-03-21 12:57:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const put_user = async (data: Object) => {
|
2026-07-30 17:07:46 +02:00
|
|
|
await check_response(await fetch(
|
2025-03-21 12:57:53 +01:00
|
|
|
get_endpoint() + "/user",
|
|
|
|
|
{ method: "PUT", body: dict_to_form(data) },
|
|
|
|
|
))
|
|
|
|
|
|
2026-09-30 19:26:21 +02:00
|
|
|
// 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()
|
2025-03-21 12:57:53 +01:00
|
|
|
}
|
2025-04-09 09:47:15 +02:00
|
|
|
|
2025-10-10 00:12:14 +02:00
|
|
|
export const logout_user = async (redirect_path: string) => {
|
2026-07-30 17:07:46 +02:00
|
|
|
await check_response(await fetch(
|
2025-10-10 00:12:14 +02:00
|
|
|
get_endpoint() + "/user/session",
|
|
|
|
|
{ method: "DELETE" },
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
window.location.pathname = redirect_path
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-09 09:47:15 +02:00
|
|
|
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
|
|
|
|
|
}
|
2026-09-30 19:26:21 +02:00
|
|
|
|
|
|
|
|
// 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<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
|
|
|
|
|
}
|