Add e-mail based login

This commit is contained in:
2025-03-25 17:58:26 +01:00
parent 7aff2a2ead
commit 3fe0b43372
17 changed files with 499 additions and 305 deletions

20
res/template/login.html Normal file
View File

@@ -0,0 +1,20 @@
{{define "login"}}<!DOCTYPE html>
<html lang="en">
<head>
{{template "meta_tags" "Login" }}
<script>
window.api_endpoint = '{{.APIEndpoint}}';
window.user = {{.User}};
window.server_hostname = "{{.Hostname}}";
</script>
<script defer src='/res/svelte/login.js?v{{cacheID}}'></script>
</head>
<body>
{{template "menu" .}}
<div id="page_body" class="page_body"></div>
{{template "analytics"}}
</body>
</html>
{{end}}

View File

@@ -20,6 +20,7 @@ export default [
"text_upload", "text_upload",
"speedtest", "speedtest",
"upload_history", "upload_history",
"login",
].map((name, index) => ({ ].map((name, index) => ({
input: `src/${name}.js`, input: `src/${name}.js`,
output: { output: {

View File

@@ -1,4 +1,4 @@
<i class="icon star" title="These options require a Persistence (€8) subscription to use">star</i> <i class="icon star" title="These options require a Persistence (€8) subscription or Prepaid plan to use">star</i>
<style> <style>
.star { .star {

View File

@@ -1,4 +1,4 @@
<i class="icon star" title="These options require a Pro (€4) subscription to use">star</i> <i class="icon star" title="These options require a Pro (€4) subscription or Prepaid plan to use">star</i>
<style> <style>
.star { .star {

View File

@@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
export let on = false export let on = false
export let icon_on = "check"
export let icon_off = "close"
export let group_first = false export let group_first = false
export let group_middle = false export let group_middle = false
export let group_last = false export let group_last = false
@@ -23,11 +25,10 @@ const click = (e: MouseEvent) => {
class:group_last class:group_last
> >
{#if on} {#if on}
<i class="icon">check</i> <i class="icon">{icon_on}</i>
{:else} {:else}
<i class="icon">close</i> <i class="icon">{icon_off}</i>
{/if} {/if}
<slot></slot> <slot></slot>
</button> </button>

View File

@@ -4,11 +4,14 @@
export type GenericResponse = { export type GenericResponse = {
value: string, value: string,
message: string, message: string,
errors?: GenericResponse[],
extra?: { [index: string]: Object },
} }
export type User = { export type User = {
username: string, username: string,
email: string, email: string,
otp_enabled: boolean,
subscription: Subscription, subscription: Subscription,
storage_space_used: number, storage_space_used: number,
filesystem_storage_used: number, filesystem_storage_used: number,

8
svelte/src/login.js Normal file
View File

@@ -0,0 +1,8 @@
import App from './login/Router.svelte';
const app = new App({
target: document.getElementById("page_body"),
props: {}
});
export default app;

View File

@@ -1,43 +1,158 @@
<script> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher, onMount } from "svelte";
import Form from "./../util/Form.svelte" import Form, { type FormConfig } from "./../util/Form.svelte"
import { check_response, get_endpoint } from "../lib/PixeldrainAPI.mjs";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
let form = { const form_login: FormConfig = {
name: "login",
fields: [ fields: [
{ {
name: "username", name: "username",
label: "Username", label: "E-mail or username",
type: "username", type: "username",
}, { }, {
name: "password", name: "password",
label: "Password", label: "Password",
type: "current_password", type: "current_password",
description:
`A password is not required to log in. If your account has an
e-mail address configured you can just enter that and press
login.`
}, },
], ],
submit_label: `<i class="icon">send</i> Login`, submit_label: `<i class="icon">send</i> Login`,
on_submit: async fields => { on_submit: async (fields) => {
const form = new FormData() username = fields.username
form.append("username", fields.username) password = fields.password
form.append("password", fields.password) return await login()
form.append("app_name", "website login")
const resp = await fetch(
window.api_endpoint+"/user/login",
{ method: "POST", body: form }
);
if(resp.status >= 400) {
return {error_json: await resp.json()}
}
let jresp = await resp.json()
dispatch("login", {key: jresp.auth_key})
return {success: true, message: "Successfully logged in"}
}, },
} }
const form_otp: FormConfig = {
fields: [
{
name: "totp",
label: "One-time password",
type: "totp",
description: `Please enter the one-time password from your authenticator app`
},
],
submit_label: `<i class="icon">send</i> Login`,
on_submit: async (fields) => {
totp = fields.totp
return await login()
},
}
// The currently rendered form
let form: FormConfig = form_login
let username = ""
let password = ""
let totp = ""
// Link login
let link_login_user_id = ""
let link_login_id = ""
let login_redirect = ""
let new_email = ""
const login = async (e?: SubmitEvent) => {
if (e !== undefined) {
e.preventDefault()
}
let fd = new FormData()
fd.set("username", username)
fd.append("app_name", "website login")
if (password !== "") {
fd.set("password", password)
}
if (link_login_user_id !== "" && link_login_id !== "") {
fd.set("link_login_user_id", link_login_user_id)
fd.set("link_login_id", link_login_id)
if (new_email !== "") {
fd.set("new_email", new_email)
}
}
if (totp !== "") {
fd.set("totp", totp)
}
if (login_redirect !== "") {
fd.set("redirect", login_redirect)
}
try {
const resp = await check_response(await fetch(
get_endpoint() + "/user/login",
{method: "POST", body: fd},
))
if (resp.value !== undefined && resp.value === "login_link_sent") {
return {
success: true,
message: "A login link was sent to your e-mail address. Click it to continue logging in",
}
}
dispatch("login", {key: resp.auth_key})
if (typeof login_redirect === "string" && login_redirect.startsWith("/")) {
console.debug("redirecting user to requested path", login_redirect)
window.location.href = window.location.protocol+"//"+window.location.host+login_redirect
} else if (window.location.pathname === "/login") {
window.location.href = "/user"
}
return {success: true, message: "Successfully logged in"}
} catch (err) {
if (err.value === "otp_required") {
form = form_otp
return
} else if (err.value === "login_link_already_sent") {
return {
success: false,
message: `A login link was already recently sent to your inbox.
Please use that one before requesting a new one. You can
only have one login link at a time. Login links stay active
for 15 minutes.`
}
}
return {success: false, message: undefined, error_json: err}
}
}
onMount(() => {
const params = new URLSearchParams(document.location.search)
if (params.get("redirect") !== null) {
login_redirect = params.get("redirect")
}
if (params.get("link_login_user_id") !== null && params.get("link_login_id") !== null) {
link_login_user_id = params.get("link_login_user_id")
link_login_id = params.get("link_login_id")
if (params.get("new_email") !== null) {
new_email = params.get("new_email")
}
login()
}
})
</script> </script>
<Form config={form}></Form> <section>
<Form config={form}/>
<br/>
<p>
If you log in with just your e-mail address then a login link will be
sent to your inbox. Click the link to log in to your account. If the
link did not arrive, please check your spam folder. Your account needs a
verified e-mail address for this login method to work.
</p>
<p>
If you have lost your password you can use this method to log in. Please
configure a new password after logging in.
</p>
</section>

View File

@@ -22,13 +22,9 @@ let page = "login"
<div class="page_content"> <div class="page_content">
{#if page === "login"} {#if page === "login"}
<Login on:login={finish_login}/> <Login on:login={finish_login}/>
<p>
If you have lost your password, you can <a
href="password_reset">request a new password here</a>.
</p>
{:else if page === "register"} {:else if page === "register"}
<Register on:login={finish_login}/> <Register on:login={finish_login}/>
{/if} {/if}
</div> </div>

View File

@@ -1,11 +1,8 @@
<script> <script lang="ts">
import { createEventDispatcher, onMount } from "svelte"; import Form, { type FormConfig } from "../util/Form.svelte"
import Form from "../util/Form.svelte" import { get_endpoint } from "../lib/PixeldrainAPI.mjs";
let dispatch = createEventDispatcher() let form: FormConfig = {
let form = {
name: "register",
fields: [ fields: [
{ {
name: "username", name: "username",
@@ -34,6 +31,7 @@ let form = {
on_submit: async fields => { on_submit: async fields => {
if (fields.password !== fields.password2) { if (fields.password !== fields.password2) {
return { return {
success: false,
error_json: { error_json: {
value: "password_verification_failed", value: "password_verification_failed",
message: "Password verification failed. Please enter the same " + message: "Password verification failed. Please enter the same " +
@@ -48,33 +46,17 @@ let form = {
form.append("password", fields.password) form.append("password", fields.password)
const resp = await fetch( const resp = await fetch(
window.api_endpoint+"/user/register", get_endpoint()+"/user/register",
{ method: "POST", body: form } { method: "POST", body: form }
); );
if(resp.status >= 400) { if(resp.status >= 400) {
return {error_json: await resp.json()} return {success: false, error_json: await resp.json()}
} }
// Register successful, now we will try logging in with the same return {
// credentials success: true,
message: "Account registration successful. Please check your inbox for an e-mail verification link"
const login_form = new FormData()
login_form.append("username", fields.username)
login_form.append("password", fields.password)
login_form.append("app_name", "website login")
const login_resp = await fetch(
window.api_endpoint+"/user/login",
{ method: "POST", body: form }
);
if(login_resp.status >= 400) {
return {error_json: await login_resp.json()}
} }
let jresp = await login_resp.json()
dispatch("login", {key: jresp.auth_key})
return {success: true, message: "Successfully registered a new account"}
}, },
} }
</script> </script>

View File

@@ -0,0 +1,36 @@
<script>
import TabMenu from "../util/TabMenu.svelte";
import Register from "./Register.svelte";
import Login from "./Login.svelte";
import { onMount } from "svelte";
import { get_user } from "../lib/PixeldrainAPI.mjs";
let pages = [
{
path: "/login",
title: "Login",
icon: "login",
component: Login,
}, {
path: "/register",
title: "Register",
icon: "settings",
component: Register,
},
]
onMount(async () => {
const params = new URLSearchParams(document.location.search)
const user = await get_user()
if (
params.get("link_login_user_id") === null &&
params.get("link_login_id") === null &&
user.username !== ""
) {
console.debug("User is already logged in, redirecting to user dashboard")
window.location.href = "/user"
}
})
</script>
<TabMenu pages={pages} title="Login" large_tabs />

View File

@@ -3,6 +3,7 @@ import { onMount } from "svelte";
import CopyButton from "../layout/CopyButton.svelte"; import CopyButton from "../layout/CopyButton.svelte";
import Form from "./../util/Form.svelte"; import Form from "./../util/Form.svelte";
import Button from "../layout/Button.svelte"; import Button from "../layout/Button.svelte";
import OtpSetup from "./OTPSetup.svelte";
let affiliate_link = window.location.protocol+"//"+window.location.host + "?ref=" + encodeURIComponent(window.user.username) let affiliate_link = window.location.protocol+"//"+window.location.host + "?ref=" + encodeURIComponent(window.user.username)
let affiliate_deny = false let affiliate_deny = false
@@ -28,16 +29,13 @@ let account_settings = {
please check your spam box too. Leave the field empty to remove please check your spam box too. Leave the field empty to remove
your current e-mail address from your account`, your current e-mail address from your account`,
separator: true separator: true
}, {
name: "password_old",
label: "Current password",
type: "current_password",
discription: `Enter your password here if you would like to change
your password.`
}, { }, {
name: "password_new1", name: "password_new1",
label: "New password", label: "New password",
type: "new_password", type: "new_password",
description: `Enter a new password here to change your account
password. If you do not wish to change your password, leave the
field empty`
}, { }, {
name: "password_new2", name: "password_new2",
label: "New password again", label: "New password again",
@@ -67,7 +65,6 @@ let account_settings = {
const form = new FormData() const form = new FormData()
form.append("email", fields.email) form.append("email", fields.email)
form.append("password_old", fields.password_old)
form.append("password_new", fields.password_new1) form.append("password_new", fields.password_new1)
form.append("username", fields.username) form.append("username", fields.username)
@@ -114,14 +111,29 @@ let delete_account = {
name: "description", name: "description",
label: "Description", label: "Description",
type: "description", type: "description",
description: `When you delete your pixeldrain account you will be description: `
<p>
When you delete your pixeldrain account you will be
logged out on all of your devices. Your account will be logged out on all of your devices. Your account will be
scheduled for deletion in seven days. If you log back in to your scheduled for deletion in seven days. If you log back in to your
account during those seven days the deletion will be canceled. account during those seven days the deletion will be canceled.
<br/><br/> </p>
<p>
The files uploaded to your account are not deleted. You need to
do that manually before deleting the account. If you do not
delete your files then they will stay available as anonymously
uploaded files and they will follow the regular file expiry
rules.
</p>
<p>
Any prepaid credit on your account will also be deleted. This is
not recoverable. We don't offer refunds on prepaid credit.
</p>
<p>
If you have an active Pro subscription you need to end that If you have an active Pro subscription you need to end that
separately through your Patreon account. Deleting your separately through your Patreon account. Deleting your
pixeldrain account will not cancel the subscription.`, pixeldrain account will not cancel the subscription.
</p>`,
}, },
], ],
submit_red: true, submit_red: true,
@@ -146,6 +158,11 @@ let delete_account = {
<Form config={account_settings}></Form> <Form config={account_settings}></Form>
</fieldset> </fieldset>
<fieldset>
<legend>Two-factor authentication</legend>
<OtpSetup/>
</fieldset>
<fieldset> <fieldset>
<legend>Affiliate settings</legend> <legend>Affiliate settings</legend>
<Form config={affiliate_settings}></Form> <Form config={affiliate_settings}></Form>

View File

@@ -37,7 +37,6 @@ const save_embed = async () => {
onMount(() => { onMount(() => {
embed_domains = window.user.file_embed_domains embed_domains = window.user.file_embed_domains
}) })
</script> </script>
<LoadingIndicator loading={loading}/> <LoadingIndicator loading={loading}/>

View File

@@ -0,0 +1,169 @@
<script lang="ts">
import { onMount } from "svelte";
import Button from "../layout/Button.svelte";
import CopyButton from "../layout/CopyButton.svelte";
import ToggleButton from "../layout/ToggleButton.svelte";
import { check_response, get_endpoint, get_user, type User } from "../lib/PixeldrainAPI.mjs";
let user: User = null
let secret = ""
let uri = ""
let qr = ""
let reveal_key = false
const generate_key = async () => {
let form = new FormData()
form.set("action", "generate")
const resp = await check_response(await fetch(
get_endpoint() + "/user/totp",
{method: "POST", body: form},
))
secret = resp.secret
uri = resp.uri
qr = get_endpoint()+"/misc/qr?text=" +encodeURIComponent(resp.uri)
console.log(resp)
}
let otp = ""
const verify = async (e: SubmitEvent) => {
e.preventDefault()
let form = new FormData()
form.set("action", "verify")
form.set("otp", otp)
form.set("secret", secret)
try {
const resp = await check_response(await fetch(
get_endpoint() + "/user/totp",
{method: "POST", body: form},
))
user.otp_enabled = true
alert("Success!")
} catch (err) {
if (err.value === "otp_incorrect") {
alert(
"The entered one-time password is not valid. It may have "+
"expired. Please return to your authenticator app and retry."+
"\n\n"+
"If it still doesn't work after that then your system clock "+
"might be incorrect. Please enable time synchronization in "+
"your operating system."
)
}
}
}
const disable = async () => {
let form = new FormData()
form.set("action", "delete")
await check_response(await fetch(
get_endpoint() + "/user/totp",
{method: "POST", body: form},
))
user.otp_enabled = false
}
onMount(async () => {
user = await get_user()
})
</script>
<div class="form">
{#if user !== null && user.otp_enabled}
<p>
Two-factor authentication is enabled on your account. Your account
is secure. If you have lost your recovery keys or authenticator
device, you can disable 2FA with the button below. After disabling
2FA you can enable it again.
<br/>
<Button click={disable} icon="close" label="Disable 2FA"/>
</p>
{:else if secret === ""}
<p>
You can improve your account security by enabling two-factor
authentication. When this is enabled you will be asked to enter a
second password when logging in. This password changes periodically.
</p>
<p>
Get started by generating an OTP key:
<Button click={generate_key} label="Generate OTP key"/>
</p>
{:else}
<h4>Key created</h4>
<p>
Now enter the secret in your Authenticator app. Most password
managers support one-time passwords. A popular option for Android is
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Google Authenticator</a>.
</p>
<p>
If your authenticator app has a QR code scanner you can scan the
code below. If not then you must enter the key manually.
</p>
<div class="qr_container">
<img src="{qr}" alt="OTP QR code" class="qr"/>
<div class="copy_container">
<Button click={() => window.location.href = uri} icon="key" label="Open in Authenticator app"/>
<CopyButton text={secret}>Copy secret key to clipboard</CopyButton>
<ToggleButton bind:on={reveal_key} icon_on="visibility" icon_off="visibility_off">Reveal secret key</ToggleButton>
{#if reveal_key}
<div class="key highlight_border">
{secret}
</div>
{/if}
</div>
</div>
<p>
Please save the secret key in your password manager or another safe
place. If you lose your authenticator app then the secret key is the
only way to gain access to your account.
</p>
<p>
Now enter the generated password to verify that the authenticator
app is working properly. This step enables two-factor authentication
on your account.
</p>
<form id="otp_verify" on:submit={verify} class="otp_form">
<input bind:value={otp} type="text" autocomplete="one-time-code" pattern={"[0-9]{6}"} required>
<Button form="otp_verify" label="Verify OTP"/>
</form>
{/if}
</div>
<style>
.qr_container {
display: flex;
flex-direction: row;
justify-content: space-around;
gap: 8px;
}
@media(max-width: 500px) {
.qr_container {
flex-direction: column;
align-items: center;
}
}
.qr {
flex: 1 1 auto;
max-width: 250px;
}
.copy_container {
flex: 1 1 auto;
display: flex;
flex-direction: column;
}
.key {
line-break: anywhere;
}
.otp_form {
display: flex;
flex-direction: row;
}
.otp_form > input {
flex: 1 1 auto;
}
</style>

View File

@@ -1,22 +1,34 @@
<script> <script lang="ts" context="module">
export type FormConfig = {
fields: FormField[],
submit_label: string
submit_red?: boolean,
on_submit: (values: {[key: string]: string}) => Promise<SubmitResult>,
}
export type FormField = {
type: string,
name?: string,
label?: string,
default_value?: string,
description?: string,
separator?: boolean,
radio_values?: string[], // Options to choose from when type is "radio"
pattern?: string, // Used for pattern matching on input fields
binding?: any
}
export type SubmitResult = {
success: boolean,
message?: string,
messages?: string[],
error_json?: GenericResponse,
}
</script>
<script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import Spinner from "./Spinner.svelte"; import Spinner from "./Spinner.svelte";
import type { GenericResponse } from "../lib/PixeldrainAPI.mjs";
export let config: FormConfig
export let config = {
fields: [
{
name: "",
label: "",
type: "",
default_value: "",
binding: null,
}
],
submit_label: "",
submit_red: false,
on_submit: async field_values => {},
}
onMount(() => { onMount(() => {
config.fields.forEach(field => { config.fields.forEach(field => {
@@ -28,15 +40,9 @@ onMount(() => {
let loading = false let loading = false
let submitted = false let submitted = false
let submit_result = { let submit_result: SubmitResult
success: false,
message: "",
messages: null,
}
let form_elem const submit = async (event: SubmitEvent) => {
let submit = async (event) => {
loading = true loading = true
event.preventDefault() event.preventDefault()
@@ -51,15 +57,13 @@ let submit = async (event) => {
} }
} else if (field.type === "description") { } else if (field.type === "description") {
field_values[field.name] = "" field_values[field.name] = ""
} else if (field.type === "captcha") {
field_values[field.name] = form_elem.getElementsByClassName("g-recaptcha-response")[0].value
} else { } else {
field_values[field.name] = field.binding.value field_values[field.name] = field.binding.value
} }
}) })
submit_result = await config.on_submit(field_values) submit_result = await config.on_submit(field_values)
if (submit_result.error_json) { if (submit_result && submit_result.error_json) {
submit_result = handle_errors(submit_result.error_json) submit_result = handle_errors(submit_result.error_json)
} }
submitted = true submitted = true
@@ -67,7 +71,8 @@ let submit = async (event) => {
loading = false loading = false
return false return false
} }
let field_label = (field) => {
const field_label = (field: string) => {
let label = "" let label = ""
config.fields.forEach(val => { config.fields.forEach(val => {
if (val.name === field) { if (val.name === field) {
@@ -76,7 +81,8 @@ let field_label = (field) => {
}) })
return label return label
} }
let handle_errors = (response) => {
const handle_errors = (response: GenericResponse) => {
console.log(response) console.log(response)
let result = {success: false, message: "", messages: null} let result = {success: false, message: "", messages: null}
@@ -86,18 +92,18 @@ let handle_errors = (response) => {
response.errors.forEach(err => { response.errors.forEach(err => {
if (err.value === "string_out_of_range") { if (err.value === "string_out_of_range") {
result.messages.push( result.messages.push(
`${field_label(err.extra.field)} is too long or too short. `${field_label(<string>err.extra.field)} is too long or too short.
It should be between ${err.extra.min_len} and It should be between ${err.extra.min_len} and
${err.extra.max_len} characters. Current length: ${err.extra.max_len} characters. Current length:
${err.extra.len}` ${err.extra.len}`
) )
} else if (err.value === "field_contains_illegal_character") { } else if (err.value === "field_contains_illegal_character") {
result.messages.push( result.messages.push(
`Character '${err.extra.char}' is not allowed in ${field_label(err.extra.field)}` `Character '${err.extra.char}' is not allowed in ${field_label(<string>err.extra.field)}`
) )
} else if (err.value === "missing_field") { } else if (err.value === "missing_field") {
result.messages.push( result.messages.push(
`${field_label(err.extra.field)} is required` `${field_label(<string>err.extra.field)} is required`
) )
} else { } else {
result.messages.push(err.message) result.messages.push(err.message)
@@ -111,8 +117,8 @@ let handle_errors = (response) => {
} }
</script> </script>
<form method="POST" on:submit={submit} bind:this={form_elem}> <form method="POST" on:submit={submit}>
{#if submitted} {#if submitted && submit_result !== undefined}
{#if submit_result.messages} {#if submit_result.messages}
<div id="submit_result" class:highlight_green={submit_result.success} class:highlight_red={!submit_result.success}> <div id="submit_result" class:highlight_green={submit_result.success} class:highlight_red={!submit_result.success}>
Something went wrong, please correct these errors before continuing:<br/> Something went wrong, please correct these errors before continuing:<br/>
@@ -129,7 +135,6 @@ let handle_errors = (response) => {
{/if} {/if}
{/if} {/if}
<div class="form"> <div class="form">
{#each config.fields as field} {#each config.fields as field}
{#if field.type !== "description"} {#if field.type !== "description"}
@@ -141,8 +146,10 @@ let handle_errors = (response) => {
id="input_{field.name}" id="input_{field.name}"
name="{field.name}" name="{field.name}"
value="{field.default_value}" value="{field.default_value}"
pattern={field.pattern}
type="text" type="text"
class="form_input"/> class="form_input"
/>
{:else if field.type === "text_area"} {:else if field.type === "text_area"}
<textarea bind:this={field.binding} <textarea bind:this={field.binding}
id="input_{field.name}" id="input_{field.name}"
@@ -155,58 +162,79 @@ let handle_errors = (response) => {
id="input_{field.name}" id="input_{field.name}"
name="{field.name}" name="{field.name}"
value="{field.default_value}" value="{field.default_value}"
pattern={field.pattern}
type="number" type="number"
class="form_input"/> class="form_input"
/>
{:else if field.type === "decimal"} {:else if field.type === "decimal"}
<input bind:this={field.binding} <input bind:this={field.binding}
id="input_{field.name}" id="input_{field.name}"
name="{field.name}" name="{field.name}"
value="{field.default_value}" value="{field.default_value}"
pattern={field.pattern}
type="number" type="number"
step="0.1" step="0.1"
class="form_input"/> class="form_input"
/>
{:else if field.type === "datetime-local"} {:else if field.type === "datetime-local"}
<input bind:this={field.binding} <input bind:this={field.binding}
id="input_{field.name}" id="input_{field.name}"
name="{field.name}" name="{field.name}"
value="{field.default_value}" value="{field.default_value}"
pattern={field.pattern}
type="datetime-local" type="datetime-local"
class="form_input"/> class="form_input"
/>
{:else if field.type === "username"} {:else if field.type === "username"}
<input bind:this={field.binding} <input bind:this={field.binding}
id="input_{field.name}" id="input_{field.name}"
name="{field.name}" name="{field.name}"
value="{field.default_value}" value="{field.default_value}"
pattern={field.pattern}
type="text" type="text"
autocomplete="username" autocomplete="username"
class="form_input"/> class="form_input"
/>
{:else if field.type === "email"} {:else if field.type === "email"}
<input bind:this={field.binding} <input bind:this={field.binding}
id="input_{field.name}" id="input_{field.name}"
name="{field.name}" name="{field.name}"
value="{field.default_value}" value="{field.default_value}"
pattern={field.pattern}
type="email" type="email"
autocomplete="email" autocomplete="email"
class="form_input"/> class="form_input"
/>
{:else if field.type === "current_password"} {:else if field.type === "current_password"}
<input bind:this={field.binding} <input bind:this={field.binding}
id="input_{field.name}" id="input_{field.name}"
name="{field.name}" name="{field.name}"
value="{field.default_value}" value="{field.default_value}"
pattern={field.pattern}
type="password" type="password"
autocomplete="current-password" autocomplete="current-password"
class="form_input"/> class="form_input"
/>
{:else if field.type === "new_password"} {:else if field.type === "new_password"}
<input bind:this={field.binding} <input bind:this={field.binding}
id="input_{field.name}" id="input_{field.name}"
name="{field.name}" name="{field.name}"
value="{field.default_value}" value="{field.default_value}"
pattern={field.pattern}
type="password" type="password"
autocomplete="new-password" autocomplete="new-password"
class="form_input"/> class="form_input"
{:else if field.type === "captcha"} />
<script src="https://www.google.com/recaptcha/api.js" async defer></script> {:else if field.type === "totp"}
<div class="g-recaptcha" data-theme="dark" data-sitekey="{field.captcha_site_key}"></div> <input bind:this={field.binding}
id="input_{field.name}"
name="{field.name}"
value="{field.default_value?field.default_value:""}"
type="text"
autocomplete="one-time-code"
pattern={"[0-9]{6}"}
class="form_input"
/>
{:else if field.type === "radio"} {:else if field.type === "radio"}
<div> <div>
{#each field.radio_values as val} {#each field.radio_values as val}

View File

@@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"net/http" "net/http"
"time"
"fornaxian.tech/log" "fornaxian.tech/log"
"fornaxian.tech/pixeldrain_api_client/pixelapi" "fornaxian.tech/pixeldrain_api_client/pixelapi"
@@ -70,185 +69,6 @@ func (wc *WebController) serveLogout(
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
} }
func (wc *WebController) registerForm(td *TemplateData, r *http.Request) (f Form) {
var err error
// This only runs on the first request
if wc.captchaSiteKey == "" {
capt, err := td.PixelAPI.GetMiscRecaptcha()
if err != nil {
log.Error("Error getting recaptcha key: %s", err)
f.SubmitMessages = []template.HTML{
"An internal server error had occurred. Registration is " +
"unavailable at the moment. Please return later",
}
return f
}
if capt.SiteKey == "" {
wc.captchaSiteKey = "none"
} else {
wc.captchaSiteKey = capt.SiteKey
}
}
// Construct the form
f = Form{
Name: "register",
Title: "Register a new pixeldrain account",
Fields: []Field{
{
Name: "username",
Label: "Username",
Description: "used for logging into your account",
Type: FieldTypeUsername,
}, {
Name: "email",
Label: "E-mail address",
Description: `not required. your e-mail address will only be
used for password resets and important account
notifications`,
Type: FieldTypeEmail,
}, {
Name: "password",
Label: "Password",
Type: FieldTypeNewPassword,
}, {
Name: "password2",
Label: "Password verification",
Description: "you need to enter your password twice so we " +
"can verify that no typing errors were made, which would " +
"prevent you from logging into your new account",
Type: FieldTypeNewPassword,
},
},
SubmitLabel: "Register",
}
if f.ReadInput(r) {
if f.FieldVal("password") != f.FieldVal("password2") {
f.SubmitMessages = []template.HTML{
"Password verification failed. Please enter the same " +
"password in both password fields"}
return f
}
// Register the user
if err = td.PixelAPI.UserRegister(
f.FieldVal("username"),
f.FieldVal("email"),
f.FieldVal("password"),
); err != nil {
formAPIError(err, &f)
return f
}
// Registration successful. Log the user in
session, err := td.PixelAPI.PostUserLogin(
f.FieldVal("username"),
f.FieldVal("password"),
"website login",
)
if err != nil {
log.Debug("Login failed: %s", err)
formAPIError(err, &f)
return
}
f.Extra.SetCookie = wc.sessionCookie(session)
f.Extra.RedirectTo = wc.loginRedirect(r)
// Request was a success
f.SubmitSuccess = true
f.SubmitMessages = []template.HTML{
`Registration completed! You can now <a href="/login">log in ` +
`to your account</a>.<br/>We're glad to have you on ` +
`board, have fun sharing!`}
}
return f
}
func (wc *WebController) loginForm(td *TemplateData, r *http.Request) (f Form) {
f = Form{
Name: "login",
Title: "Log in to your pixeldrain account",
Fields: []Field{
{
Name: "username",
Label: "Username",
Type: FieldTypeUsername,
}, {
Name: "password",
Label: "Password",
Type: FieldTypeCurrentPassword,
},
},
SubmitLabel: "Login",
PostFormHTML: template.HTML(
`<p>If you don't have a pixeldrain account yet, you can ` +
`<a href="/register">register here</a>. No e-mail address is ` +
`required.</p>` +
`<p>Forgot your password? If your account has a valid e-mail ` +
`address you can <a href="/password_reset">request a new ` +
`password here</a>.</p>`,
),
}
// If the user is already logged in we redirect to the target page
// immediately
if td.Authenticated {
f.Extra.RedirectTo = wc.loginRedirect(r)
return f
}
if f.ReadInput(r) {
if session, err := td.PixelAPI.PostUserLogin(
f.FieldVal("username"),
f.FieldVal("password"),
"website login",
); err != nil {
log.Debug("Login failed: %s", err)
formAPIError(err, &f)
} else {
// Request was a success
f.SubmitSuccess = true
f.SubmitMessages = []template.HTML{"Success!"}
// Set the autentication cookie
f.Extra.SetCookie = wc.sessionCookie(session)
f.Extra.RedirectTo = wc.loginRedirect(r)
}
}
return f
}
func (wc *WebController) sessionCookie(session pixelapi.UserSession) *http.Cookie {
return &http.Cookie{
Name: "pd_auth_key",
Value: session.AuthKey.String(),
Path: "/",
Expires: time.Now().AddDate(50, 0, 0),
Domain: wc.config.SessionCookieDomain,
// Strict means the Cookie will only be sent when the user
// reaches a page by a link from the same domain. Lax means any
// page on the domain gets the cookie and None means embedded
// content also gets the cookie.
//
// Users who see pixeldrain links in iframes also expect their
// accounts to be logged in so we need to use None
SameSite: http.SameSiteNoneMode,
Secure: true,
}
}
func (wc *WebController) loginRedirect(r *http.Request) string {
if redirect := r.URL.Query().Get("redirect"); redirect == "checkout" {
return "/user/prepaid/deposit#deposit"
} else {
return "/user"
}
}
func (wc *WebController) passwordResetForm(td *TemplateData, r *http.Request) (f Form) { func (wc *WebController) passwordResetForm(td *TemplateData, r *http.Request) (f Form) {
f = Form{ f = Form{
Name: "password_reset", Name: "password_reset",

View File

@@ -156,10 +156,9 @@ func New(r *httprouter.Router, prefix string, conf Config) (wc *WebController) {
{GET, "speedtest" /* */, wc.serveTemplate("speedtest", handlerOpts{})}, {GET, "speedtest" /* */, wc.serveTemplate("speedtest", handlerOpts{})},
// User account pages // User account pages
{GET, "register" /* */, wc.serveForm(wc.registerForm, handlerOpts{NoEmbed: true})}, {GET, "login" /* */, wc.serveTemplate("login", handlerOpts{NoEmbed: true})},
{PST, "register" /* */, wc.serveForm(wc.registerForm, handlerOpts{NoEmbed: true})}, {GET, "register" /* */, wc.serveTemplate("login", handlerOpts{NoEmbed: true})},
{GET, "login" /* */, wc.serveForm(wc.loginForm, handlerOpts{NoEmbed: true})},
{PST, "login" /* */, wc.serveForm(wc.loginForm, handlerOpts{NoEmbed: true})},
{GET, "password_reset" /* */, wc.serveForm(wc.passwordResetForm, handlerOpts{NoEmbed: true})}, {GET, "password_reset" /* */, wc.serveForm(wc.passwordResetForm, handlerOpts{NoEmbed: true})},
{PST, "password_reset" /* */, wc.serveForm(wc.passwordResetForm, handlerOpts{NoEmbed: true})}, {PST, "password_reset" /* */, wc.serveForm(wc.passwordResetForm, handlerOpts{NoEmbed: true})},
{GET, "logout" /* */, wc.serveTemplate("logout", handlerOpts{Auth: true, NoEmbed: true})}, {GET, "logout" /* */, wc.serveTemplate("logout", handlerOpts{Auth: true, NoEmbed: true})},