Overhaul user settings page

This commit is contained in:
2022-10-18 14:30:50 +02:00
parent f1c6b3adf8
commit 71a34fc881
15 changed files with 751 additions and 635 deletions

View File

@@ -178,6 +178,7 @@ onMount(get_reporters);
on:delete={e => delete_reporter(e.detail)}>
</AbuseReporterTable>
</div>
<br/>
<style>
.toolbar {

View File

@@ -187,6 +187,10 @@ onDestroy(() => {
<br/>
<ServerDiagnostics running_since={status.cpu_profile_running_since} on:refresh={() => getStats(lastOrder)}/>
<br/>
<a class="button" href="/admin/globals">
<i class="icon">edit</i>
Global settings
</a>
<section>
<table>

View File

@@ -1,98 +1,45 @@
<script>
import AbuseReporters from "./AbuseReporters.svelte"
import AbuseReports from "./AbuseReports.svelte"
import IpBans from "./IPBans.svelte"
import IPBans from "./IPBans.svelte"
import Home from "./Home.svelte"
import { onMount } from "svelte";
import BlockFiles from "./BlockFiles.svelte";
import Subscriptions from "./Subscriptions.svelte";
import Footer from "../layout/Footer.svelte";
import TabMenu from "../util/TabMenu.svelte";
import UserManagement from "./UserManagement.svelte";
import EmailReporters from "./EmailReporters.svelte";
let page = ""
let navigate = (path, title) => {
page = path
window.document.title = title+" ~ pixeldrain"
window.history.pushState(
{}, window.document.title, "/admin/"+path
)
}
onMount(() => {
let newpage = window.location.pathname.substring(window.location.pathname.lastIndexOf("/")+1)
if (newpage === "admin") {
newpage = "status"
}
page = newpage
})
let pages = [
{
path: "/admin",
title: "Status",
icon: "home",
component: Home,
}, {
path: "/admin/block_files",
title: "Block Files",
icon: "block",
component: BlockFiles,
}, {
path: "/admin/abuse_reports",
title: "User Reports",
icon: "flag",
component: AbuseReports,
}, {
path: "/admin/email_reporters",
title: "E-mail Reporters",
icon: "email",
component: EmailReporters,
}, {
path: "/admin/ip_bans",
title: "IP Bans",
icon: "remove_circle",
component: IPBans,
}, {
path: "/admin/user_management",
title: "User Management",
icon: "person",
component: UserManagement,
},
]
</script>
<header>
<h1>Admin Panel</h1>
<div class="tab_bar">
<a class="button"
href="/admin"
class:button_highlight={page === "status"}
on:click|preventDefault={() => {navigate("status", "Status")}}>
<i class="icon">home</i><br/>
Status
</a>
<a class="button"
href="/admin/block_files"
class:button_highlight={page === "block_files"}
on:click|preventDefault={() => {navigate("block_files", "Block files")}}>
<i class="icon">block</i><br/>
Block files
</a>
<a class="button"
href="/admin/abuse_reports"
class:button_highlight={page === "abuse_reports"}
on:click|preventDefault={() => {navigate("abuse_reports", "Abuse reports")}}>
<i class="icon">flag</i><br/>
User reports
</a>
<a class="button"
href="/admin/abuse_reporters"
class:button_highlight={page === "abuse_reporters"}
on:click|preventDefault={() => {navigate("abuse_reporters", "Abuse reporters")}}>
<i class="icon">email</i><br/>
E-mail reporters
</a>
<a class="button"
href="/admin/ip_bans"
class:button_highlight={page === "ip_bans"}
on:click|preventDefault={() => {navigate("ip_bans", "IP bans")}}>
<i class="icon">remove_circle</i><br/>
IP bans
</a>
<a class="button"
href="/admin/subscriptions"
class:button_highlight={page === "subscriptions"}
on:click|preventDefault={() => {navigate("subscriptions", "Subscriptions")}}>
<i class="icon">receipt_long</i><br/>
Subscriptions
</a>
<a class="button" href="/admin/globals">
<i class="icon">edit</i><br/>
Global settings
</a>
</div>
</header>
<div id="page_content" class="page_content">
{#if page === "status"}
<Home></Home>
{:else if page === "block_files"}
<BlockFiles></BlockFiles>
{:else if page === "abuse_reports"}
<AbuseReports></AbuseReports>
{:else if page === "abuse_reporters"}
<AbuseReporters></AbuseReporters>
{:else if page === "ip_bans"}
<IpBans></IpBans>
{:else if page === "subscriptions"}
<Subscriptions></Subscriptions>
{/if}
</div>
<Footer/>
<TabMenu pages={pages} title="Admin Panel"/>

View File

@@ -1,7 +1,7 @@
<script>
import { onMount } from "svelte";
import Euro from "../util/Euro.svelte";
import Form from "./../util/Form.svelte";
import Form from "../util/Form.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte";
let loading = true
@@ -176,6 +176,11 @@ onMount(get_coupons)
<LoadingIndicator loading={loading}/>
<section>
<h2>Impersonate user</h2>
<div class="highlight_shaded">
<Form config={impersonate_form}></Form>
</div>
<h2>Give user credit</h2>
<p>
This adds credit to a user's account. You only need to enter one of
@@ -185,11 +190,6 @@ onMount(get_coupons)
<Form config={credit_form}></Form>
</div>
<h2>Impersonate user</h2>
<div class="highlight_shaded">
<Form config={impersonate_form}></Form>
</div>
<h2>Create coupon codes</h2>
<div class="highlight_shaded">
<Form config={coupon_form}></Form>

View File

@@ -302,11 +302,9 @@ const node_click = (index) => {
<div id="directory_element">
<div bind:this={directorySorters} id="sorters" class="directory_sorters">
{#each tableColumns as col}
<!-- <div style="min-width: {col.width}"> -->
<button style="min-width: {col.width}" on:click={sortBy(col.field)} class="sorter_button">
{col.name}
</button>
<!-- </div> -->
{/each}
</div>
<div bind:this={directoryArea} on:scroll={onScroll} id="directory_area" class="directory_area">

View File

@@ -0,0 +1,135 @@
<script>
import { onMount } from "svelte";
import { formatDataVolume } from "../util/Formatting.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte";
import ProgressBar from "../util/ProgressBar.svelte";
import SuccessMessage from "../util/SuccessMessage.svelte";
let loading = false
let success_message
const update = async () => {
loading = true
const form = new FormData()
form.append("update", "limits")
form.append("hotlinking_enabled", hotlinking)
form.append("transfer_cap", transfer_cap*1e9)
try {
const resp = await fetch(
window.api_endpoint+"/user/subscription",
{ method: "PUT", body: form },
)
if(resp.status >= 400) {
let json = await resp.json()
throw json.message
}
window.user.hotlinking_enabled = hotlinking
window.user.monthly_transfer_cap = transfer_cap*1e9
success_message.set(true, "Sharing settings updated")
} catch (err) {
success_message.set(false, "Failed to update subscription: "+err)
} finally {
loading = false
}
}
let hotlinking = window.user.hotlinking_enabled
let toggle_hotlinking = () => {
hotlinking = !hotlinking
update()
}
let transfer_cap = window.user.monthly_transfer_cap / 1e9
let transfer_used = 0
let load_transfer_used = () => {
let today = new Date()
let start = new Date()
start.setDate(start.getDate() - 30)
fetch(
window.api_endpoint + "/user/time_series/transfer_paid" +
"?start=" + start.toISOString() +
"&end=" + today.toISOString() +
"&interval=60"
).then(resp => {
if (!resp.ok) { return Promise.reject("Error: " + resp.status); }
return resp.json();
}).then(resp => {
transfer_used = resp.amounts.reduce((acc, cur) => { return acc + cur }, 0)
}).catch(e => {
console.error("Error requesting time series: " + e);
})
}
onMount(() => {
load_transfer_used()
})
</script>
<LoadingIndicator loading={loading}/>
<section>
<h2>Bandwidth sharing</h2>
<SuccessMessage bind:this={success_message}></SuccessMessage>
<button on:click={toggle_hotlinking}>
{#if hotlinking}
<i class="icon green">check</i> ON (click to turn off)
{:else}
<i class="icon red">close</i> OFF (click to turn on)
{/if}
</button>
<p>
When bandwidth sharing is enabled all the bandwidth that your files
use will be subtracted from your data cap. Advertisements will be
disabled on the download pages for your files and download speed
will be unlimited. The rate limiting captcha for files is also
disabled when bandwidth sharing is on. You can directly embed your
file's download link anywhere, you don't need to use the file viewer
page.
</p>
<h2>Bill shock limit</h2>
<p>
Billshock limit in gigabytes per month (1 TB = 1000 GB). Set to 0 to disable.
</p>
<form on:submit|preventDefault={update} class="billshock_container">
<input type="number" bind:value={transfer_cap} step="100" min="0"/>
<div style="margin: 0.5em;">GB</div>
<button type="submit">
<i class="icon">save</i> Save
</button>
</form>
<p>
Bandwidth used in the last 30 days: {formatDataVolume(transfer_used, 3)},
new limit: {formatDataVolume(transfer_cap*1e9, 3)}
</p>
<ProgressBar used={transfer_used} total={transfer_cap*1e9}></ProgressBar>
<p>
The billshock limit limits how much bandwidth your account can use
in a 30 day window. When this limit is reached files will show ads
again and can only be downloaded from the file viewer page. This is
mostly useful for prepaid plans, but it works for patreon plans too.
Set to 0 to disable the limit.
</p>
</section>
<style>
.green {
color: var(--highlight_color);
}
.red {
color: var(--danger_color);
}
.billshock_container {
display: flex;
flex-direction: row;
align-items: center;
}
</style>

View File

@@ -0,0 +1,154 @@
<script>
import { onMount } from "svelte";
import Euro from "../util/Euro.svelte";
import { formatDate } from "../util/Formatting.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte";
let loading = false
let credit_amount = 10
const checkout = async (network) => {
loading = true
const form = new FormData()
form.set("amount", credit_amount*1e6)
form.set("network", network)
try {
const resp = await fetch(
window.api_endpoint+"/btcpay/deposit",
{ method: "POST", body: form },
)
if(resp.status >= 400) {
let json = await resp.json()
throw json.message
}
window.location = (await resp.json()).checkout_url
} catch (err) {
alert(err)
} finally {
loading = false
}
}
let show_expired = false
let invoices = []
const load_invoices = async () => {
loading = true
try {
const resp = await fetch(window.api_endpoint+"/btcpay/invoice")
if(resp.status >= 400) {
throw new Error((await resp.json()).message)
}
let invoices_tmp = await resp.json()
invoices_tmp.forEach(row => {
row.time = new Date(row.time)
})
invoices_tmp.sort((a, b) => {
return b.time - a.time
})
invoices = invoices_tmp
} catch (err) {
console.error(err)
alert(err)
} finally {
loading = false
}
};
onMount(() => {
load_invoices()
})
</script>
<LoadingIndicator loading={loading}/>
<section>
<h2>Deposit credits</h2>
<p>
You can deposit credit on your pixeldrain account with Bitcoin,
Lightning network (<a
href="https://btcpay.pixeldrain.com/embed/uS2mbWjXUuaAqMh8XLjkjwi8oehFuxeBZxekMxv68LN/BTC/ln"
target="_blank">node info</a>) and Dogecoin. You must pay the full
amount as stated on the invoice, else your payment will fail.
</p>
<p>
Do note that it is not possible to withdraw coins from your
pixeldrain account. It's not a wallet. Any amount of money you
deposit has to be used up.
</p>
<div style="text-align: center;">
Deposit amount €
<input type="number" bind:value={credit_amount} min="1"/>
<br/>
Pay with:<br/>
<button on:click={() => {checkout("btc")}}>
<span class="icon_unicode"></span> Bitcoin
</button>
<button on:click={() => {checkout("btc_lightning")}}>
<i class="icon">bolt</i> Lightning network
</button>
<button on:click={() => {checkout("doge")}}>
<span class="icon_unicode">Ð</span> Dogecoin
</button>
</div>
<h3>Open invoices</h3>
<div class="table_scroll">
<table style="text-align: left;">
<thead>
<tr>
<td>Created</td>
<td>Amount</td>
<td>Status</td>
<td></td>
</tr>
</thead>
<tbody>
{#each invoices as row (row.id)}
{#if row.status === "New" ||
row.status === "InvoiceCreated" ||
row.status === "InvoiceProcessing" ||
show_expired
}
<tr>
<td>{formatDate(row.time, true, true, false)}</td>
<td><Euro amount={row.amount}></Euro></td>
<td>
{#if row.status === "InvoiceCreated"}
New (waiting for payment)
{:else if row.status === "InvoiceProcessing"}
Payment received, waiting for confirmations
{:else if row.status === "InvoiceSettled"}
Paid
{:else if row.status === "InvoiceExpired"}
Expired
{:else}
{row.status}
{/if}
</td>
<td>
{#if row.status === "New" || row.status === "InvoiceCreated"}
<a href={row.checkout_url} class="button button_highlight">
<i class="icon">paid</i> Pay
</a>
{/if}
</td>
</tr>
{/if}
{/each}
</tbody>
</table>
<div style="text-align: center;">
<button on:click={() => {show_expired = !show_expired}}>
{#if show_expired}
Hide
{:else}
Show
{/if}
expired and settled invoices
</button>
</div>
</div>
</section>

View File

@@ -0,0 +1,88 @@
<script>
import { onMount } from "svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte";
import SuccessMessage from "../util/SuccessMessage.svelte";
let loading = false
let success_message
// Embedding settings
let embed_domains = ""
const save_embed = async () => {
loading = true
const form = new FormData()
form.append("domains", embed_domains)
try {
const resp = await fetch(
window.api_endpoint+"/user/file_embed",
{ method: "PUT", body: form }
);
if(resp.status >= 400) {
let json = await resp.json()
console.debug(json)
throw json.message
}
success_message.set(true, "Changes saved")
} catch(err) {
success_message.set(false, err)
} finally {
loading = false
}
}
onMount(() => {
embed_domains = window.user.file_embed_domains
})
</script>
<LoadingIndicator loading={loading}/>
<section>
<h2>Embedding controls</h2>
<SuccessMessage bind:this={success_message}></SuccessMessage>
{#if !window.user.subscription.file_viewer_branding}
<div class="highlight_red">
Sharing settings are not available for your account. Subscribe to
the Persistence plan or higher to enable these features.
</div>
{:else if !window.user.hotlinking_enabled}
<div class="highlight_red">
To use embedding restrictions bandwidth sharing needs to be enabled.
Enable bandwidth sharing on the
<a href="/user/sharing/bandwidth">bandwidth sharing page</a>.
</div>
{/if}
<p>
Here you can control which websites are allowed to embed your files in
their web pages. If a website that is not on this list tries to embed
one of your files the request will be blocked.
</p>
<p>
The list should be formatted as a list of domain names separated by a
space. Like this: 'pixeldrain.com google.com twitter.com'
</p>
<form class="form_row" on:submit|preventDefault={save_embed}>
<div class="shrink">Domain names:</div>
<input class="grow" bind:value={embed_domains} type="text"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
</section>
<style>
.form_row {
display: inline-flex;
flex-direction: row;
width: 100%;
align-items: center;
}
.grow {
flex: 1 1 auto;
}
.shrink {
flex: 0 0 auto;
}
</style>

View File

@@ -1,10 +1,10 @@
<script>
import { onMount } from "svelte";
import FilePicker from "../file_viewer/FilePicker.svelte";
import CustomBanner from "../file_viewer/CustomBanner.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte";
import SuccessMessage from "../util/SuccessMessage.svelte";
import ThemePicker from "../util/ThemePicker.svelte";
import { onMount } from "svelte";
let loading = false
let success_message
@@ -74,33 +74,6 @@ let save = async () => {
}
}
// Embedding settings
let embed_domains = ""
const save_embed = async () => {
loading = true
const form = new FormData()
form.append("domains", embed_domains)
try {
const resp = await fetch(
window.api_endpoint+"/user/file_embed",
{ method: "PUT", body: form }
);
if(resp.status >= 400) {
let json = await resp.json()
console.debug(json)
throw json.message
}
success_message.set(true, "Changes saved")
} catch(err) {
success_message.set(false, err)
} finally {
loading = false
}
}
onMount(() => {
// The fields are undefined when they're empty. So we need to check if each
// field is defined before converting to a string
@@ -113,16 +86,13 @@ onMount(() => {
footer_image = b.footer_image ? b.footer_image : ""
footer_link = b.footer_link ? b.footer_link : ""
}
embed_domains = window.user.file_embed_domains
})
</script>
<LoadingIndicator loading={loading}/>
<section>
<h2>Sharing settings</h2>
<h2>File viewer branding</h2>
{#if !window.user.subscription.file_viewer_branding}
<div class="highlight_red">
Sharing settings are not available for your account. Subscribe to
@@ -130,13 +100,13 @@ onMount(() => {
</div>
{:else if !window.user.hotlinking_enabled}
<div class="highlight_red">
To use the sharing settings bandwidth sharing needs to be enabled.
Enable bandwidth sharing on the
<a href="/user/subscription">subscription page</a>.
To use custom file viewer branding bandwidth sharing needs to be
enabled. Enable bandwidth sharing on the
<a href="/user/sharing/bandwidth">bandwidth sharing page</a>.
</div>
{/if}
<h3>File viewer branding</h3>
<SuccessMessage bind:this={success_message}></SuccessMessage>
<p>
You can change the appearance of your file viewer pages. The images you
choose here will be loaded each time someone visits one of your files.
@@ -148,7 +118,7 @@ onMount(() => {
should use APNG or WebP. Avoid using animated GIFs as they are very slow
to load.
</p>
<h4>Theme</h4>
<h3>Theme</h3>
<p>
Choose a theme for your download pages. This theme will override the
theme preference of the person viewing the file. Set to 'None' to let
@@ -159,7 +129,7 @@ onMount(() => {
on:theme_change={e => {theme = e.detail; save()}}>
</ThemePicker>
<h4>Header image</h4>
<h3>Header image</h3>
<p>
Will be shown above the file. Maximum height is 90px. Will be shrunk if
larger. You can also add a link to open when the visitor clicks the
@@ -174,8 +144,8 @@ onMount(() => {
Remove
</button>
<br/>
Header image link:<br/>
<form class="form_row" on:submit|preventDefault={save}>
<div class="shrink">Header image link:</div>
<input class="grow" bind:value={header_link} type="text" placeholder="https://"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
@@ -186,7 +156,7 @@ onMount(() => {
</div>
{/if}
<h4>Background image</h4>
<h3>Background image</h3>
<p>
This image will be shown behind the file which is being viewed. I
recommend choosing something dark and not too distracting. Try to keep
@@ -207,7 +177,7 @@ onMount(() => {
</div>
{/if}
<h4>Footer image</h4>
<h3>Footer image</h3>
<p>
Will be shown below the file. Maximum height is 90px. Will be shrunk if
larger.
@@ -221,8 +191,8 @@ onMount(() => {
Remove
</button>
<br/>
Footer image link:<br/>
<form class="form_row" on:submit|preventDefault={save}>
<div class="shrink">Footer image link:</div>
<input class="grow" bind:value={footer_link} type="text" placeholder="https://"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
@@ -231,22 +201,6 @@ onMount(() => {
<CustomBanner src={"/api/file/"+footer_image} link={footer_link}></CustomBanner>
</div>
{/if}
<br/>
<h3>Embedding controls</h3>
<p>
Here you can control which websites are allowed to embed your files in
their web pages. If a website that is not on this list tries to embed
one of your files the request will be blocked.
</p>
<p>
The list should be formatted as a list of domain names separated by a
space. Like this: 'pixeldrain.com google.com twitter.com'
</p>
<form class="form_row" on:submit|preventDefault={save_embed}>
<div class="shrink">Domain names:</div>
<input class="grow" bind:value={embed_domains} type="text"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
</section>
<FilePicker

View File

@@ -1,5 +1,4 @@
<script>
import { onMount } from "svelte";
import Home from "./Home.svelte";
import AccountSettings from "./AccountSettings.svelte";
import APIKeys from "./APIKeys.svelte";
@@ -7,116 +6,85 @@ import Transactions from "./Transactions.svelte";
import Subscription from "./Subscription.svelte";
import ConnectApp from "./ConnectApp.svelte";
import ActivityLog from "./ActivityLog.svelte";
import SharingSettings from "./SharingSettings.svelte";
import Footer from "../layout/Footer.svelte";
import DepositCredit from "./DepositCredit.svelte";
import TabMenu from "../util/TabMenu.svelte";
import BandwidthSharing from "./BandwidthSharing.svelte";
import EmbeddingControls from "./EmbeddingControls.svelte";
import PageBranding from "./PageBranding.svelte";
let page = ""
let navigate = (path, title) => {
page = path
window.document.title = title+" ~ pixeldrain"
window.history.pushState(
{}, window.document.title, "/user/"+path
)
}
let get_page = () => {
let newpage = window.location.pathname.substring(window.location.pathname.lastIndexOf("/")+1)
if (newpage === "user") {
newpage = "home"
}
page = newpage
}
onMount(() => {
get_page()
})
let pages = [
{
path: "/user/home",
title: "My Home",
icon: "home",
component: Home,
}, {
path: "/user/settings",
title: "Settings",
icon: "settings",
component: AccountSettings,
}, {
path: "/user/sharing",
title: "Sharing",
icon: "share",
subpages: [
{
path: "/user/sharing/bandwidth",
title: "Bandwidth Sharing",
icon: "share",
component: BandwidthSharing,
}, {
path: "/user/sharing/branding",
title: "Page Branding",
icon: "palette",
component: PageBranding,
}, {
path: "/user/sharing/embedding",
title: "Embedding Controls",
icon: "code",
component: EmbeddingControls,
},
],
}, {
path: "/user/connect_app",
title: "Apps",
icon: "app_registration",
component: ConnectApp,
}, {
path: "/user/api_keys",
title: "API Keys",
icon: "vpn_key",
component: APIKeys,
}, {
path: "/user/activity",
title: "Activity Log",
icon: "list",
component: ActivityLog,
}, {
path: "/user/prepaid",
title: "Prepaid",
icon: "receipt_long",
hidden: window.user.subscription.type === "patreon",
subpages: [
{
path: "/user/prepaid/deposit",
title: "Deposit credit",
icon: "account_balance_wallet",
component: DepositCredit,
}, {
path: "/user/prepaid/subscriptions",
title: "Subscriptions",
icon: "shopping_cart",
component: Subscription,
}, {
path: "/user/prepaid/transactions",
title: "Transactions",
icon: "receipt",
component: Transactions,
},
],
},
]
</script>
<svelte:window on:popstate={get_page} />
<header>
<h1>Welcome home, {window.user.username}!</h1>
<div class="tab_bar">
<a class="button"
href="/user"
class:button_highlight={page === "home"}
on:click|preventDefault={() => {navigate("home", "My home")}}>
<i class="icon">home</i><br/>
My home
</a>
<a class="button"
href="/user/settings"
class:button_highlight={page === "settings"}
on:click|preventDefault={() => {navigate("settings", "Settings")}}>
<i class="icon">settings</i><br/>
Settings
</a>
<a class="button"
href="/user/sharing"
class:button_highlight={page === "sharing"}
on:click|preventDefault={() => {navigate("sharing", "Sharing")}}>
<i class="icon">share</i><br/>
Sharing
</a>
<a class="button"
href="/user/connect_app"
class:button_highlight={page === "connect_app"}
on:click|preventDefault={() => {navigate("connect_app", "Apps")}}>
<i class="icon">app_registration</i><br/>
Apps
</a>
<a class="button"
href="/user/api_keys"
class:button_highlight={page === "api_keys"}
on:click|preventDefault={() => {navigate("api_keys", "API keys")}}>
<i class="icon">vpn_key</i><br/>
Keys
</a>
<a class="button"
href="/user/activity"
class:button_highlight={page === "activity"}
on:click|preventDefault={() => {navigate("activity", "Activity log")}}>
<i class="icon">list</i><br/>
Activity log
</a>
<a class="button"
href="/user/subscription"
class:button_highlight={page === "subscription"}
on:click|preventDefault={() => {navigate("subscription", "Subscription")}}>
<i class="icon">shopping_cart</i><br/>
Subscription
</a>
{#if window.user.subscription.type !== "patreon"}
<a class="button"
href="/user/transactions"
class:button_highlight={page === "transactions"}
on:click|preventDefault={() => {navigate("transactions", "Transactions")}}>
<i class="icon">receipt_long</i><br/>
Transactions
</a>
{/if}
</div>
</header>
<div id="page_content" class="page_content">
{#if page === "home"}
<Home/>
{:else if page === "settings"}
<AccountSettings/>
{:else if page === "sharing"}
<SharingSettings/>
{:else if page === "api_keys"}
<APIKeys/>
{:else if page === "activity"}
<ActivityLog/>
{:else if page === "connect_app"}
<ConnectApp/>
{:else if page === "transactions"}
<Transactions/>
{:else if page === "subscription"}
<Subscription/>
{/if}
</div>
<Footer/>
<TabMenu pages={pages} title="Welcome home, {window.user.username}!"/>

View File

@@ -1,31 +1,18 @@
<script>
import Euro from "../util/Euro.svelte"
import ProgressBar from "../util/ProgressBar.svelte";
import { onMount } from "svelte";
import { formatDataVolume } from "../util/Formatting.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte";
import SuccessMessage from "../util/SuccessMessage.svelte";
let loading = false
let subscription = window.user.subscription.id
let success_message
let result = ""
let result_success = false
const update = async (update_field) => {
const update = async () => {
loading = true
const form = new FormData()
if (update_field === "subscription") {
form.append("update", "subscription")
form.append("subscription", subscription)
} else if (update_field === "limits") {
form.append("update", "limits")
form.append("hotlinking_enabled", hotlinking)
form.append("transfer_cap", transfer_cap*1e9)
} else {
console.error("Invalid update type", update_field)
return
}
try {
const resp = await fetch(
@@ -37,75 +24,32 @@ const update = async (update_field) => {
throw json.message
}
result_success = true
result = "Subscription updated"
success_message.set(true, "Subscription updated")
} catch (err) {
result_success = false
result = "Failed to update subscription: "+err
success_message.set(false, "Failed to update subscription: "+err)
} finally {
loading = false
}
}
let hotlinking = window.user.hotlinking_enabled
let transfer_cap = window.user.monthly_transfer_cap / 1e9
let transfer_used = 0
let load_tranfer_used = () => {
let today = new Date()
let start = new Date()
start.setDate(start.getDate() - 30)
fetch(
window.api_endpoint + "/user/time_series/transfer_paid" +
"?start=" + start.toISOString() +
"&end=" + today.toISOString() +
"&interval=60"
).then(resp => {
if (!resp.ok) { return Promise.reject("Error: " + resp.status); }
return resp.json();
}).then(resp => {
transfer_used = resp.amounts.reduce((acc, cur) => { return acc + cur }, 0)
}).catch(e => {
console.error("Error requesting time series: " + e);
})
}
onMount(() => {
load_tranfer_used()
})
</script>
<LoadingIndicator loading={loading}/>
<section>
<h2>Manage subscription</h2>
{#if window.user.subscription.type !== "patreon"}
<p>
Current account balance: <Euro amount={window.user.balance_micro_eur}></Euro>
</p>
<p>
When your prepaid subscription is active you will be charged daily
based on usage. Hotlink bandwidth is charged per TB based on the
usage of the previous day. The amount charged for storage each day
is your storage usage at the end of the day multiplied by the
storage price (€4 / TB) and divided by the average number of days in
a month (30.4375). So if you have exactly 1 TB on your account you
will be charged €0.131416838 per day.
</p>
<p>
The prepaid subscription will stay active for as long as you have
credit on your account. When you reach negative balance the
subscription will automatically end. You will need a positive
balance to activate the subscription again.
When your prepaid subscription is active you will be charged daily based
on usage. The prepaid subscription will stay active for as long as you
have credit on your account. When you reach negative balance the
subscription will automatically end. You will need a positive balance to
activate the subscription again.
</p>
<h3>Prepaid plans</h3>
{#if result !== ""}
<div class:highlight_green={result_success} class:highlight_red={!result_success}>
{result}
</div>
{/if}
<SuccessMessage bind:this={success_message}/>
<div class="feat_table">
<div>
@@ -123,7 +67,7 @@ onMount(() => {
<div class="feat_normal round_tr" class:feat_highlight={subscription === "prepaid"}>
<ul>
<li>Base price of €1 per month</li>
<li>4 per TB per month for storage</li>
<li>3 per TB per month for storage</li>
<li>€2 per TB for data transfer</li>
<li>Files never expire as long as subscription is active</li>
</ul>
@@ -142,32 +86,11 @@ onMount(() => {
{/if}
</div>
<div class="feat_normal" class:feat_highlight={subscription === "prepaid_temp_storage_120d"}>
<ul>
<li>Base price of €1 per month</li>
<li>€1 per TB per month for storage</li>
<li>€2 per TB for data transfer</li>
<li>Files expire 120 days after the last time they're viewed</li>
</ul>
</div>
</div>
<div>
<div class="feat_label" class:feat_highlight={subscription === "prepaid_temp_storage_60d"}>
60 days storage<br/>
{#if subscription === "prepaid_temp_storage_60d"}
Currently active
{:else}
<button on:click={() => {subscription = "prepaid_temp_storage_60d"; update("subscription")}}>
<i class="icon">attach_money</i>
Activate
</button>
{/if}
</div>
<div class="feat_normal" class:feat_highlight={subscription === "prepaid_temp_storage_60d"}>
<ul>
<li>Base price of €1 per month</li>
<li>€0.50 per TB per month for storage</li>
<li>€2 per TB for data transfer</li>
<li>Files expire 60 days after the last time they're viewed</li>
<li>Files expire 120 days after the last time they're viewed</li>
</ul>
</div>
</div>
@@ -190,64 +113,9 @@ onMount(() => {
</div>
</div>
</div>
{/if}
<h3>Bandwidth sharing</h3>
{#if hotlinking}
<button on:click={() => { hotlinking = false; update("limits") }}>
<i class="icon green">check</i> ON (click to turn off)
</button>
{:else}
<button on:click={() => { hotlinking = true; update("limits") }}>
<i class="icon red">close</i> OFF (click to turn on)
</button>
{/if}
<p>
When bandwidth sharing is enabled all the bandwidth that your files
use will be subtracted from your data cap. Advertisements will be
disabled on the download pages for your files and download speed
will be unlimited. The rate limiting captcha for files is also
disabled when bandwidth sharing is on. You can directly embed your
file's download link anywhere, you don't need to use the file viewer
page.
</p>
<h3>Bill shock limit</h3>
<p>
Billshock limit in gigabytes per month (1 TB = 1000 GB). Set to 0 to disable.
</p>
<form on:submit|preventDefault={() => {update("limits")}} class="billshock_container">
<input type="number" bind:value={transfer_cap} step="100" min="0"/>
<div style="margin: 0.5em;">GB</div>
<button type="submit">
<i class="icon">save</i> Save
</button>
</form>
Bandwidth used in the last 30 days: {formatDataVolume(transfer_used, 3)},
new limit: {formatDataVolume(transfer_cap*1e9, 3)}
<ProgressBar used={transfer_used} total={transfer_cap*1e9}></ProgressBar>
<p>
The billshock limit limits how much bandwidth your account can use
in a 30 day window. When this limit is reached files will show ads
again and can only be downloaded from the file viewer page. This is
mostly useful for prepaid plans, but it works for patreon plans too.
Set to 0 to disable the limit.
</p>
</section>
<style>
.green {
color: var(--highlight_color);
}
.red {
color: var(--danger_color);
}
.billshock_container {
display: flex;
flex-direction: row;
align-items: center;
}
.feat_table {
display: flex;
flex-direction: column;

View File

@@ -31,7 +31,9 @@ const load_transactions = async () => {
let month = {
rows: await resp.json(),
total_subscription_charge: 0,
total_storage_used: 0,
total_storage_charge: 0,
total_bandwidth_used: 0,
total_bandwidth_charge: 0,
total_deposited: 0,
total_deducted: 0,
@@ -41,10 +43,15 @@ const load_transactions = async () => {
row.time = new Date(row.time)
month.total_deposited += row.deposit_amount
month.total_subscription_charge += row.subscription_charge
month.total_storage_used += row.storage_used
month.total_storage_charge += row.storage_charge
month.total_bandwidth_used += row.bandwidth_used
month.total_bandwidth_charge += row.bandwidth_charge
month.total_deducted += row.subscription_charge + row.storage_charge + row.bandwidth_charge
})
month.total_storage_used /= month.rows.length
transactions = month
} catch (err) {
alert(err)
@@ -71,162 +78,33 @@ const next_month = () => {
load_transactions()
}
let credit_amount = 10
const checkout = async (network) => {
loading = true
const form = new FormData()
form.set("amount", credit_amount*1e6)
form.set("network", network)
try {
const resp = await fetch(
window.api_endpoint+"/btcpay/deposit",
{ method: "POST", body: form },
)
if(resp.status >= 400) {
let json = await resp.json()
throw json.message
}
window.location = (await resp.json()).checkout_url
} catch (err) {
alert(err)
} finally {
loading = false
}
}
let show_expired = false
let invoices = []
const load_invoices = async () => {
loading = true
try {
const resp = await fetch(window.api_endpoint+"/btcpay/invoice")
if(resp.status >= 400) {
throw new Error((await resp.json()).message)
}
let invoices_tmp = await resp.json()
invoices_tmp.forEach(row => {
row.time = new Date(row.time)
})
invoices_tmp.sort((a, b) => {
return b.time - a.time
})
invoices = invoices_tmp
} catch (err) {
console.error(err)
alert(err)
} finally {
loading = false
}
};
onMount(() => {
let now = new Date()
year = now.getFullYear()
month = now.getMonth()+1
load_transactions()
load_invoices()
})
</script>
<LoadingIndicator loading={loading}/>
<section>
<h2>Deposit credits</h2>
<p>
You can deposit credit on your pixeldrain account with Bitcoin,
Lightning network (<a
href="https://btcpay.pixeldrain.com/embed/uS2mbWjXUuaAqMh8XLjkjwi8oehFuxeBZxekMxv68LN/BTC/ln"
target="_blank">node info</a>) and Dogecoin. You must pay the full
amount as stated on the invoice, else your payment will fail.
</p>
<p>
Do note that it is not possible to withdraw coins from your
pixeldrain account. It's not a wallet. Any amount of money you
deposit has to be used up.
</p>
<div style="text-align: center;">
Deposit amount €
<input type="number" bind:value={credit_amount} min="1"/>
<br/>
Pay with:<br/>
<button on:click={() => {checkout("btc")}}>
<span class="icon_unicode"></span> Bitcoin
</button>
<button on:click={() => {checkout("btc_lightning")}}>
<i class="icon">bolt</i> Lightning network
</button>
<button on:click={() => {checkout("doge")}}>
<span class="icon_unicode">Ð</span> Dogecoin
</button>
</div>
<h3>Open invoices</h3>
<div class="table_scroll">
<table style="text-align: left;">
<thead>
<tr>
<td>Created</td>
<td>Amount</td>
<td>Status</td>
<td></td>
</tr>
</thead>
<tbody>
{#each invoices as row (row.id)}
{#if row.status === "New" ||
row.status === "InvoiceCreated" ||
row.status === "InvoiceProcessing" ||
show_expired
}
<tr>
<td>{formatDate(row.time, true, true, false)}</td>
<td><Euro amount={row.amount}></Euro></td>
<td>
{#if row.status === "InvoiceCreated"}
New (waiting for payment)
{:else if row.status === "InvoiceProcessing"}
Payment received, waiting for confirmations
{:else if row.status === "InvoiceSettled"}
Paid
{:else if row.status === "InvoiceExpired"}
Expired
{:else}
{row.status}
{/if}
</td>
<td>
{#if row.status === "New" || row.status === "InvoiceCreated"}
<a href={row.checkout_url} class="button button_highlight">
<i class="icon">paid</i> Pay
</a>
{/if}
</td>
</tr>
{/if}
{/each}
</tbody>
</table>
<div style="text-align: center;">
<button on:click={() => {show_expired = !show_expired}}>
{#if show_expired}
Hide
{:else}
Show
{/if}
expired and settled invoices
</button>
</div>
</div>
<h2>Transaction log</h2>
<p>
Here is a log of all transactions on your account balance.
Here is a log of all transactions on your account balance. Usage is
calculated per day. The storage charge is divided by the average number
of months in a day (30.4375).
</p>
<p>
Example: If you have 2 TB stored on your pixeldrain account at €3 per TB
then the daily charge will be:<br/>
2 TB * € 3 = € 6<br/>
€ 6 / 30.4375 = € 0.197125 per day<br/>
Similarly the subscription charge of €1 per month is also charged at € 1
/ 30.4375 = € 0.032854 per day.
</p>
<h3>{month_str}</h3>
@@ -242,11 +120,23 @@ onMount(() => {
</button>
</div>
<ul>
<li>Subscription charge: <Euro amount={transactions.total_subscription_charge}></Euro></li>
<li>Storage charge: <Euro amount={transactions.total_storage_charge}></Euro></li>
<li>Bandwidth charge: <Euro amount={transactions.total_bandwidth_charge}></Euro></li>
<li>Total charge: <Euro amount={transactions.total_deducted}></Euro></li>
<li>Deposited: <Euro amount={transactions.total_deposited}></Euro></li>
<li>
Subscription charge: <Euro amount={transactions.total_subscription_charge} precision="4"/>
</li>
<li>
Storage charge: <Euro amount={transactions.total_storage_charge} precision="4"/>
(used {formatDataVolume(transactions.total_storage_used, 3)})
</li>
<li>
Bandwidth charge: <Euro amount={transactions.total_bandwidth_charge} precision="4"/>
(used {formatDataVolume(transactions.total_bandwidth_used, 3)})
</li>
<li>
Total charge: <Euro amount={transactions.total_deducted} precision="4"/>
</li>
<li>
Deposited: <Euro amount={transactions.total_deposited} precision="4"/>
</li>
</ul>
<div class="table_scroll">
@@ -276,10 +166,10 @@ onMount(() => {
<tr>
<td>{formatDate(row.time, true, true, false)}</td>
<td><Euro amount={row.new_balance}></Euro></td>
<td><Euro amount={row.subscription_charge} precision="4"></Euro></td>
<td><Euro amount={row.storage_charge} precision="4"></Euro></td>
<td><Euro amount={row.subscription_charge} precision="6"></Euro></td>
<td><Euro amount={row.storage_charge} precision="6"></Euro></td>
<td>{formatDataVolume(row.storage_used, 3)}</td>
<td><Euro amount={row.bandwidth_charge} precision="4"></Euro></td>
<td><Euro amount={row.bandwidth_charge} precision="6"></Euro></td>
<td>{formatDataVolume(row.bandwidth_used, 3)}</td>
<td><Euro amount={row.deposit_amount}></Euro></td>
</tr>

View File

@@ -50,7 +50,6 @@ Chart.defaults.animation.duration = 500;
Chart.defaults.animation.easing = "linear";
onMount(() => {
console.log(legend)
chart_object = new Chart(
chart_element.getContext("2d"),
{

View File

@@ -0,0 +1,107 @@
<script>
import { onMount } from "svelte";
import Footer from "../layout/Footer.svelte";
export let title = ""
export let pages = []
let navigate = (path, title) => {
window.document.title = title+" ~ pixeldrain"
window.history.pushState({}, window.document.title, path)
get_page()
}
let get_page = () => {
current_page = null
current_subpage = null
pages.forEach(page => {
if (window.location.pathname.endsWith(page.path)) {
current_page = page
}
if (page.subpages) {
page.subpages.forEach(subpage => {
if (window.location.pathname.endsWith(subpage.path)) {
current_page = page
current_subpage = subpage
}
})
}
})
// If no page is active, default to home
if (!current_page) {
current_page = pages[0]
}
if (!current_subpage && current_page.subpages) {
current_subpage = current_page.subpages[0]
}
console.log("Page", current_page)
console.log("Subpage", current_subpage)
pages = pages
}
let current_page = null
let current_subpage = null
onMount(() => get_page())
</script>
<svelte:window on:popstate={get_page} />
<header>
<h1>{title}</h1>
<div class="tab_bar">
{#each pages as page}
{#if !page.hidden}
<a class="button"
href="{page.path}"
class:button_highlight={current_page && page.path === current_page.path}
on:click|preventDefault={() => {navigate(page.path, page.title)}}>
<i class="icon">{page.icon}</i><br/>
{page.title}
</a>
{/if}
{/each}
</div>
</header>
<div id="page_content" class="page_content">
{#if current_page}
{#if current_page.subpages}
<div class="tab_bar submenu">
{#each current_page.subpages as page}
{#if !page.hidden}
<a class="button"
href="{page.path}"
class:button_highlight={current_subpage && page.path === current_subpage.path}
on:click|preventDefault={() => {navigate(page.path, page.title)}}>
<i class="icon">{page.icon}</i><br/>
{page.title}
</a>
{/if}
{/each}
</div>
{#if current_subpage}
<svelte:component this={current_subpage.component} />
{/if}
{:else}
<svelte:component this={current_page.component} />
{/if}
{/if}
</div>
<Footer/>
<style>
.submenu{
border-bottom: 2px solid var(--separator);
}
</style>

View File

@@ -178,11 +178,14 @@ func New(
{GET, "user/home" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/settings" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/sharing" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/sharing/*p" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/api_keys" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/activity" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/connect_app" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/transactions" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/subscription" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/prepaid" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/prepaid/*p" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/confirm_email" /* */, wc.serveEmailConfirm},
{GET, "user/password_reset_confirm" /**/, wc.serveForm(wc.passwordResetConfirmForm, handlerOpts{NoEmbed: true})},
{PST, "user/password_reset_confirm" /**/, wc.serveForm(wc.passwordResetConfirmForm, handlerOpts{NoEmbed: true})},
@@ -200,10 +203,10 @@ func New(
{GET, "admin" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/status" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/block_files" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/abuse_reporters" /**/, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/email_reporters" /**/, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/abuse_reports" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/ip_bans" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/subscriptions" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/user_management" /**/, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/globals" /* */, wc.serveForm(wc.adminGlobalsForm, handlerOpts{Auth: true})},
{PST, "admin/globals" /* */, wc.serveForm(wc.adminGlobalsForm, handlerOpts{Auth: true})},