Remove old user and block file pages

This commit is contained in:
2021-09-23 22:21:27 +02:00
parent d896d8794c
commit 3c478720c9
7 changed files with 79 additions and 563 deletions

View File

@@ -1,92 +0,0 @@
function loadGraph(graph, stat, minutes, interval) {
let today = new Date()
let start = new Date()
start.setMinutes(start.getMinutes() - minutes)
fetch(
apiEndpoint + "/user/time_series/" + stat +
"?start=" + start.toISOString() +
"&end=" + today.toISOString() +
"&interval=" + interval
).then(resp => {
if (!resp.ok) { return Promise.reject("Error: " + resp.status); }
return resp.json();
}).then(resp => {
resp.timestamps.forEach((val, idx) => {
let date = new Date(val);
let dateStr = ("00" + (date.getMonth() + 1)).slice(-2);
dateStr += "-" + ("00" + date.getDate()).slice(-2);
dateStr += " " + ("00" + date.getHours()).slice(-2);
dateStr += ":" + ("00" + date.getMinutes()).slice(-2);
resp.timestamps[idx] = " " + dateStr + " "; // Poor man's padding
});
graph.data.labels = resp.timestamps;
graph.data.datasets[0].data = resp.amounts;
graph.update();
document.getElementById("time_start").innerText = resp.timestamps[0];
document.getElementById("time_end").innerText = resp.timestamps.slice(-1)[0];
let total = 0
resp.amounts.forEach(e => { total += e; });
if (stat == "views") {
document.getElementById("total_views").innerText = formatThousands(total);
} else if (stat == "downloads") {
document.getElementById("total_downloads").innerText = formatThousands(total);
} else if (stat == "bandwidth") {
document.getElementById("total_bandwidth").innerText = formatDataVolume(total, 3);
} else if (stat == "direct_bandwidth") {
document.getElementById("total_direct_bandwidth").innerText = formatDataVolume(total, 3);
}
}).catch(e => {
console.error("Error requesting time series: " + e);
})
}
function loadDirectBW() {
let today = new Date()
let start = new Date()
start.setDate(start.getDate() - 30)
fetch(
apiEndpoint + "/user/time_series/direct_bandwidth" +
"?start=" + start.toISOString() +
"&end=" + today.toISOString() +
"&interval=60"
).then(resp => {
if (!resp.ok) { return Promise.reject("Error: " + resp.status); }
return resp.json();
}).then(resp => {
let total = resp.amounts.reduce((accum, val) => accum += val, 0);
document.getElementById("direct_bandwidth_progress").style.width = (total / window.user.subscription.direct_linking_bandwidth) * 100 + "%"
document.getElementById("direct_bandwidth_text").innerText = formatDataVolume(total, 3) + " out of " + formatDataVolume(window.user.subscription.direct_linking_bandwidth, 3)
document.getElementById("storage_progress").style.width = (window.user.storage_space_used / window.user.subscription.storage_space) * 100 + "%"
document.getElementById("storage_text").innerText = formatDataVolume(window.user.storage_space_used, 3) + " out of " + formatDataVolume(window.user.subscription.storage_space, 3)
}).catch(e => {
console.error("Error requesting time series: " + e);
})
}
let graphViews = drawGraph(document.getElementById("views_chart"), "Views", "number");
let graphDownloads = drawGraph(document.getElementById("downloads_chart"), "Downloads", "number");
let graphBandwidth = drawGraph(document.getElementById("bandwidth_chart"), "Bandwidth", "bytes");
let graphDirectBandwidth = drawGraph(document.getElementById("direct_bandwidth_chart"), "Direct Bandwidth", "bytes");
let graphTimeout = null;
function updateGraphs(minutes, interval, live) {
if (graphTimeout !== null) { clearTimeout(graphTimeout) }
if (live) {
graphTimeout = setTimeout(() => { updateGraphs(minutes, interval, true) }, 10000)
}
loadGraph(graphViews, "views", minutes, interval);
loadGraph(graphDownloads, "downloads", minutes, interval);
loadGraph(graphBandwidth, "bandwidth", minutes, interval);
loadGraph(graphDirectBandwidth, "direct_bandwidth", minutes, interval);
loadDirectBW()
}
// Default
updateGraphs(1440, 1, true);

View File

@@ -3,24 +3,14 @@
<head>
{{template "meta_tags" .User.Username}}
{{template "user_style" .}}
<script>
var apiEndpoint = '{{.APIEndpoint}}';
window.api_endpoint = '{{.APIEndpoint}}';
window.highlight_color = '#{{.Style.HighlightColor.RGB}}';
window.user = {{.User}};
</script>
<style>
.progress_bar_outer {
background-color: var(--layer_1_color);
width: 100%;
height: 3px;
}
.progress_bar_inner {
background-color: var(--highlight_color);
height: 100%;
width: 0;
transition: width 1s;
}
</style>
<link rel='stylesheet' href='/res/svelte/user_home.css'>
<script defer src='/res/svelte/user_home.js'></script>
</head>
<body>
@@ -28,148 +18,10 @@
<h1>Welcome home, {{.User.Username}}!</h1>
<div class="page_content">
<div class="limit_width">
<h2>Account information</h2>
<ul>
<li>Username: {{.User.Username}}</li>
<li>E-mail address: {{.User.Email}}</li>
<li>
Supporter level: {{.User.Subscription.Name}}
{{if eq .User.Subscription.Type "patreon"}}
(<a href="https://www.patreon.com/join/pixeldrain/checkout?edit=1">Manage subscription</a>)
{{end}}
<ul>
<li>Advertisements when viewing files: {{if .User.Subscription.DisableAdDisplay}}No{{else}}Yes{{end}}</li>
<li>Advertisements on your uploaded files: {{if .User.Subscription.DisableAdsOnFiles}}No{{else}}Yes{{end}}</li>
{{if gt .User.Subscription.FileExpiryDays 0}}
<li>Files expire after {{.User.Subscription.FileExpiryDays}} days</li>
{{else}}
<li>Files never expire</li>
{{end}}
</ul>
</li>
</ul>
<h3>Limits</h3>
Storage: <span id="storage_text"></span><br/>
<div class="progress_bar_outer">
<div id="storage_progress" class="progress_bar_inner" style="width: 0%;"></div>
</div>
<br/>
Direct link bandwidth: <span id="direct_bandwidth_text"></span>
(<a href="/#direct_linking">More information about direct linking</a>)
<div class="progress_bar_outer">
<div id="direct_bandwidth_progress" class="progress_bar_inner" style="width: 0%;"></div>
</div>
<br/>
<h3>Settings</h3>
<div style="text-align: center;">
<a href="/user/settings" class="button button_highlight">
<i class="icon">edit</i>
Change account settings
</a>
<a href="/user/export/files" class="button">
<i class="icon">list</i>
Export uploaded files to CSV
</a>
<a href="/user/export/lists" class="button">
<i class="icon">list</i>
Export created lists to CSV
</a>
</div>
<h2>Statistics</h2>
<p>
Here you can see how often your files are viewed, downloaded
and how much bandwidth they consume. The buttons at the top
can be pressed to adjust the timeframe. If you choose 'Day'
the statistics will be updated periodically. No need to
refresh the page.
</p>
</div>
<div class="highlight_dark">
<button onclick="updateGraphs(1440, 1, true);">Day</button>
<button onclick="updateGraphs(10080, 60, false);">Week</button>
<button onclick="updateGraphs(20160, 60, false);">Two Weeks</button>
<button onclick="updateGraphs(43200, 1440, false);">Month</button>
<button onclick="updateGraphs(131400, 1440, false);">Quarter</button>
<button onclick="updateGraphs(262800, 1440, false);">Half-year</button>
<button onclick="updateGraphs(525600, 1440, false);">Year</button>
</div>
<div class="limit_width">
<h3>Views</h3>
<p>
A view is counted when someone visits the download page of one
of your files. Views are unique per user per file.
</p>
</div>
<div class="chart-container" style="position: relative; width: 100%; height: 140px;">
<canvas id="views_chart"></canvas>
</div>
<div class="limit_width">
<h3>Downloads</h3>
<p>
Downloads are counted when a user clicks the download button
on one of your files. It does not matter whether the
download is completed or not, only the start of the download
is counted.
</p>
</div>
<div class="chart-container" style="position: relative; width: 100%; height: 140px;">
<canvas id="downloads_chart"></canvas>
</div>
<div class="limit_width">
<h3>Bandwidth</h3>
<p>
This is how much bandwidth your files are using in total.
Bandwidth is used when a file is tranferred from a
pixeldrain server to a user who is downloading the file.
When a 5 MB file is downloaded 8 times it has used 40 MB of
bandwidth.
</p>
</div>
<div class="chart-container" style="position: relative; width: 100%; height: 140px;">
<canvas id="bandwidth_chart"></canvas>
</div>
<div class="limit_width">
<h3>Direct link bandwidth</h3>
<p>
When a file is downloaded without going through pixeldrain's
download page it counts as a direct download. Because direct
downloads cost us bandwidth and don't generate any ad
revenue we have to limit them. When your direct link
bandwidth runs out people will be asked to do a test before
they can download your files. See our
<a href="/#pro">subscription options</a> to get more direct
linking bandwidth.
</p>
</div>
<div class="chart-container" style="position: relative; width: 100%; height: 140px;">
<canvas id="direct_bandwidth_chart"></canvas>
</div>
<div class="highlight_dark">
Total usage from <span id="time_start"></span> to <span id="time_end"></span><br/>
<span id="total_views"></span> views,
<span id="total_downloads"></span> downloads,
<span id="total_bandwidth"></span> bandwidth and
<span id="total_direct_bandwidth"></span> direct link bandwidth
</div>
</div>
<div id="page_content" class="page_content"></div>
{{template "page_bottom" .}}
{{template "analytics"}}
<script src="/res/script/Chart.min.js"></script>
<script>
var apiEndpoint = '{{.APIEndpoint}}';
var highlightColor = '#{{.Style.HighlightColor.RGB}}';
{{template `util.js`}}
{{template `drawGraph.js`}}
{{template `user_home.js`}}
</script>
</body>
</html>
{{end}}

View File

@@ -1,27 +0,0 @@
{{define "user_home_svelte"}}<!DOCTYPE html>
<html lang="en">
<head>
{{template "meta_tags" .User.Username}}
{{template "user_style" .}}
<script>
window.api_endpoint = '{{.APIEndpoint}}';
window.highlight_color = '#{{.Style.HighlightColor.RGB}}';
window.user = {{.User}};
</script>
<link rel='stylesheet' href='/res/svelte/user_home.css'>
<script defer src='/res/svelte/user_home.js'></script>
</head>
<body>
{{template "page_top" .}}
<h1>Welcome home, {{.User.Username}}!</h1>
<div id="page_content" class="page_content"></div>
{{template "page_bottom" .}}
{{template "analytics"}}
</body>
</html>
{{end}}

View File

@@ -90,65 +90,3 @@ func (wc *WebController) adminGlobalsForm(td *TemplateData, r *http.Request) (f
}
return f
}
func (wc *WebController) adminAbuseForm(td *TemplateData, r *http.Request) (f Form) {
if !td.Authenticated || !td.User.IsAdmin {
return Form{Title: ";-)"}
}
f = Form{
Name: "admin_file_removal",
Title: "Admin file removal",
PreFormHTML: template.HTML("<p>Paste any pixeldrain file links in here to remove them</p>"),
Fields: []Field{
{
Name: "text",
Label: "Files to delete",
Type: FieldTypeTextarea,
}, {
Name: "type",
Label: "Type",
DefaultValue: "unknown",
Type: FieldTypeRadio,
RadioValues: []string{
"unknown",
"copyright",
"child_abuse",
"terrorism",
"gore",
"malware",
},
}, {
Name: "reporter",
Label: "Reporter",
DefaultValue: "Anonymous tip",
Type: FieldTypeText,
},
},
BackLink: "/admin",
SubmitLabel: "Submit",
}
if f.ReadInput(r) {
resp, err := td.PixelAPI.AdminBlockFiles(
f.FieldVal("text"),
f.FieldVal("type"),
f.FieldVal("reporter"),
)
if err != nil {
formAPIError(err, &f)
return
}
successMsg := template.HTML("The following files were blocked:<br/><ul>")
for _, v := range resp.FilesBlocked {
successMsg += template.HTML("<li>pixeldrain.com/u/" + v + "</li>")
}
successMsg += "<ul>"
// Request was a success
f.SubmitSuccess = true
f.SubmitMessages = []template.HTML{successMsg}
}
return f
}

View File

@@ -1,14 +1,60 @@
package webcontroller
import (
"fmt"
"html/template"
"net/http"
"time"
"fornaxian.tech/pixeldrain_api_client/pixelapi"
"github.com/Fornaxian/log"
"github.com/julienschmidt/httprouter"
)
// formAPIError makes it easier to display errors returned by the pixeldrain
// API. TO make use of this function the form fields should be named exactly the
// same as the API parameters
func formAPIError(err error, f *Form) {
fieldLabel := func(name string) string {
for _, v := range f.Fields {
if v.Name == name {
return v.Label
}
}
return name
}
if err, ok := err.(pixelapi.Error); ok {
if err.StatusCode == "multiple_errors" {
for _, err := range err.Errors {
// Modify the message to make it more user-friendly
if err.StatusCode == "string_out_of_range" {
err.Message = fmt.Sprintf(
"%s is too long or too short. Should be between %v and %v characters. Current length: %v",
fieldLabel(err.Extra["field"].(string)),
err.Extra["min_len"],
err.Extra["max_len"],
err.Extra["len"],
)
} else if err.StatusCode == "field_contains_illegal_character" {
err.Message = fmt.Sprintf(
"Character '%v' is not allowed in %s",
err.Extra["char"],
fieldLabel(err.Extra["field"].(string)),
)
}
f.SubmitMessages = append(f.SubmitMessages, template.HTML(err.Message))
}
} else {
f.SubmitMessages = append(f.SubmitMessages, template.HTML(err.Message))
}
} else {
log.Error("Error submitting form: %s", err)
f.SubmitMessages = []template.HTML{"Internal Server Error"}
}
}
func (wc *WebController) serveLogout(
w http.ResponseWriter,
r *http.Request,
@@ -271,3 +317,27 @@ func (wc *WebController) passwordResetConfirmForm(td *TemplateData, r *http.Requ
}
return f
}
func (wc *WebController) serveEmailConfirm(
w http.ResponseWriter,
r *http.Request,
p httprouter.Params,
) {
var err error
var status string
err = wc.api.PutUserEmailResetConfirm(r.FormValue("key"))
if err != nil && err.Error() == "not_found" {
status = "not_found"
} else if err != nil {
log.Error("E-mail reset fail: %s", err)
status = "internal_error"
} else {
status = "success"
}
td := wc.newTemplateData(w, r)
td.Other = status
wc.templates.Get().ExecuteTemplate(w, "email_confirm", td)
}

View File

@@ -1,220 +0,0 @@
package webcontroller
import (
"fmt"
"html"
"html/template"
"net/http"
"fornaxian.tech/pixeldrain_api_client/pixelapi"
"github.com/Fornaxian/log"
"github.com/julienschmidt/httprouter"
)
// formAPIError makes it easier to display errors returned by the pixeldrain
// API. TO make use of this function the form fields should be named exactly the
// same as the API parameters
func formAPIError(err error, f *Form) {
fieldLabel := func(name string) string {
for _, v := range f.Fields {
if v.Name == name {
return v.Label
}
}
return name
}
if err, ok := err.(pixelapi.Error); ok {
if err.StatusCode == "multiple_errors" {
for _, err := range err.Errors {
// Modify the message to make it more user-friendly
if err.StatusCode == "string_out_of_range" {
err.Message = fmt.Sprintf(
"%s is too long or too short. Should be between %v and %v characters. Current length: %v",
fieldLabel(err.Extra["field"].(string)),
err.Extra["min_len"],
err.Extra["max_len"],
err.Extra["len"],
)
} else if err.StatusCode == "field_contains_illegal_character" {
err.Message = fmt.Sprintf(
"Character '%v' is not allowed in %s",
err.Extra["char"],
fieldLabel(err.Extra["field"].(string)),
)
}
f.SubmitMessages = append(f.SubmitMessages, template.HTML(err.Message))
}
} else {
f.SubmitMessages = append(f.SubmitMessages, template.HTML(err.Message))
}
} else {
log.Error("Error submitting form: %s", err)
f.SubmitMessages = []template.HTML{"Internal Server Error"}
}
}
func (wc *WebController) serveUserSettings(
w http.ResponseWriter,
r *http.Request,
p httprouter.Params,
) {
w.Header().Set("X-Frame-Options", "DENY")
td := wc.newTemplateData(w, r)
if !td.Authenticated {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
td.Title = "Account settings"
td.Other = struct {
PasswordForm Form
EmailForm Form
UsernameForm Form
}{
PasswordForm: wc.passwordForm(td, r),
EmailForm: wc.emailForm(td, r),
UsernameForm: wc.usernameForm(td, r),
}
wc.templates.Get().ExecuteTemplate(w, "user_settings", td)
}
func (wc *WebController) passwordForm(td *TemplateData, r *http.Request) (f Form) {
f = Form{
Name: "password_change",
Title: "Change password",
Fields: []Field{
{
Name: "old_password",
Label: "Old Password",
Type: FieldTypeCurrentPassword,
}, {
Name: "new_password",
Label: "New Password",
Type: FieldTypeNewPassword,
}, {
Name: "new_password2",
Label: "New Password again",
Description: "we need you to repeat your password so you " +
"won't be locked out of your account if you make a " +
"typing error",
Type: FieldTypeNewPassword,
},
},
SubmitLabel: "Submit",
}
if f.ReadInput(r) {
if f.FieldVal("new_password") != f.FieldVal("new_password2") {
f.SubmitMessages = []template.HTML{
"Password verification failed. Please enter the same " +
"password in both new password fields"}
return f
}
// Passwords match, send the request and fill in the response in the
// form
if err := td.PixelAPI.PutUserPassword(
f.FieldVal("old_password"),
f.FieldVal("new_password"),
); err != nil {
formAPIError(err, &f)
} else {
// Request was a success
f.SubmitSuccess = true
f.SubmitMessages = []template.HTML{"Success! Your password has been updated"}
}
}
return f
}
func (wc *WebController) emailForm(td *TemplateData, r *http.Request) (f Form) {
f = Form{
Name: "email_change",
Title: "Change e-mail address",
Fields: []Field{
{
Name: "new_email",
Label: "New e-mail address",
Description: `we will send an e-mail to the new address to
verify that it's real. The address will be saved once the
link in the message is clicked. If the e-mail doesn't arrive
right away please check your spam box too`,
Type: FieldTypeEmail,
},
},
SubmitLabel: "Submit",
}
if f.ReadInput(r) {
if err := td.PixelAPI.PutUserEmailReset(
f.FieldVal("new_email"),
false,
); err != nil {
formAPIError(err, &f)
} else {
// Request was a success
f.SubmitSuccess = true
f.SubmitMessages = []template.HTML{"Success! E-mail sent"}
}
}
return f
}
func (wc *WebController) serveEmailConfirm(
w http.ResponseWriter,
r *http.Request,
p httprouter.Params,
) {
var err error
var status string
err = wc.api.PutUserEmailResetConfirm(r.FormValue("key"))
if err != nil && err.Error() == "not_found" {
status = "not_found"
} else if err != nil {
log.Error("E-mail reset fail: %s", err)
status = "internal_error"
} else {
status = "success"
}
td := wc.newTemplateData(w, r)
td.Other = status
wc.templates.Get().ExecuteTemplate(w, "email_confirm", td)
}
func (wc *WebController) usernameForm(td *TemplateData, r *http.Request) (f Form) {
f = Form{
Name: "username_change",
Title: "Change username",
Fields: []Field{
{
Name: "new_username",
Label: "New username",
Description: "changing your username also changes the name " +
"used to log in. If you forget your username you can " +
"still log in using your e-mail address if you have one " +
"configured",
Type: FieldTypeUsername,
},
},
SubmitLabel: "Submit",
}
if f.ReadInput(r) {
if err := td.PixelAPI.PutUserUsername(f.FieldVal("new_username")); err != nil {
formAPIError(err, &f)
} else {
// Request was a success
f.SubmitSuccess = true
f.SubmitMessages = []template.HTML{template.HTML(
"Success! You are now " + html.EscapeString(f.FieldVal("new_username")),
)}
}
}
return f
}

View File

@@ -159,7 +159,6 @@ func New(
{PST, "password_reset" /* */, wc.serveForm(wc.passwordResetForm, handlerOpts{NoEmbed: true})},
{GET, "logout" /* */, wc.serveTemplate("logout", handlerOpts{Auth: true, NoEmbed: true})},
{PST, "logout" /* */, wc.serveLogout},
{GET, "user_old" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/files" /* */, wc.serveTemplate("user_files", handlerOpts{Auth: true})},
{GET, "user/lists" /* */, wc.serveTemplate("user_lists", handlerOpts{Auth: true})},
{GET, "user/buckets" /* */, wc.serveTemplate("user_buckets", handlerOpts{Auth: true})},
@@ -168,11 +167,9 @@ func New(
{GET, "user/export/lists" /**/, wc.serveUserExportLists},
// User account settings
{GET, "user" /* */, wc.serveTemplate("user_home_svelte", handlerOpts{Auth: true})},
{GET, "user/settings" /* */, wc.serveTemplate("user_home_svelte", handlerOpts{Auth: true})},
{GET, "user/api_keys" /* */, wc.serveTemplate("user_home_svelte", handlerOpts{Auth: true})},
{GET, "user/settings_old" /* */, wc.serveUserSettings},
{PST, "user/settings_old" /* */, wc.serveUserSettings},
{GET, "user" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/settings" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/api_keys" /* */, 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})},
@@ -192,8 +189,6 @@ func New(
{GET, "admin/ip_bans" /* */, 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})},
{GET, "admin/abuse" /* */, wc.serveForm(wc.adminAbuseForm, handlerOpts{Auth: true})},
{PST, "admin/abuse" /* */, wc.serveForm(wc.adminAbuseForm, handlerOpts{Auth: true})},
// Advertising related
{GET, "click/:id" /* */, wc.serveAdClick},