Add e-mail based login
This commit is contained in:
20
res/template/login.html
Normal file
20
res/template/login.html
Normal 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}}
|
@@ -20,6 +20,7 @@ export default [
|
||||
"text_upload",
|
||||
"speedtest",
|
||||
"upload_history",
|
||||
"login",
|
||||
].map((name, index) => ({
|
||||
input: `src/${name}.js`,
|
||||
output: {
|
||||
|
@@ -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>
|
||||
.star {
|
||||
|
@@ -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>
|
||||
.star {
|
||||
|
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
export let on = false
|
||||
export let icon_on = "check"
|
||||
export let icon_off = "close"
|
||||
export let group_first = false
|
||||
export let group_middle = false
|
||||
export let group_last = false
|
||||
@@ -23,11 +25,10 @@ const click = (e: MouseEvent) => {
|
||||
class:group_last
|
||||
>
|
||||
{#if on}
|
||||
<i class="icon">check</i>
|
||||
<i class="icon">{icon_on}</i>
|
||||
{:else}
|
||||
<i class="icon">close</i>
|
||||
<i class="icon">{icon_off}</i>
|
||||
{/if}
|
||||
|
||||
<slot></slot>
|
||||
</button>
|
||||
|
||||
|
@@ -4,11 +4,14 @@
|
||||
export type GenericResponse = {
|
||||
value: string,
|
||||
message: string,
|
||||
errors?: GenericResponse[],
|
||||
extra?: { [index: string]: Object },
|
||||
}
|
||||
|
||||
export type User = {
|
||||
username: string,
|
||||
email: string,
|
||||
otp_enabled: boolean,
|
||||
subscription: Subscription,
|
||||
storage_space_used: number,
|
||||
filesystem_storage_used: number,
|
||||
|
8
svelte/src/login.js
Normal file
8
svelte/src/login.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import App from './login/Router.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById("page_body"),
|
||||
props: {}
|
||||
});
|
||||
|
||||
export default app;
|
@@ -1,43 +1,158 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import Form from "./../util/Form.svelte"
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import Form, { type FormConfig } from "./../util/Form.svelte"
|
||||
import { check_response, get_endpoint } from "../lib/PixeldrainAPI.mjs";
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
|
||||
let form = {
|
||||
name: "login",
|
||||
const form_login: FormConfig = {
|
||||
fields: [
|
||||
{
|
||||
name: "username",
|
||||
label: "Username",
|
||||
label: "E-mail or username",
|
||||
type: "username",
|
||||
}, {
|
||||
name: "password",
|
||||
label: "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`,
|
||||
on_submit: async fields => {
|
||||
const form = new FormData()
|
||||
form.append("username", fields.username)
|
||||
form.append("password", fields.password)
|
||||
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"}
|
||||
on_submit: async (fields) => {
|
||||
username = fields.username
|
||||
password = fields.password
|
||||
return await login()
|
||||
},
|
||||
}
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
@@ -22,13 +22,9 @@ let page = "login"
|
||||
|
||||
<div class="page_content">
|
||||
{#if page === "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>
|
||||
<Login on:login={finish_login}/>
|
||||
{:else if page === "register"}
|
||||
<Register on:login={finish_login}/>
|
||||
<Register on:login={finish_login}/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script>
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import Form from "../util/Form.svelte"
|
||||
<script lang="ts">
|
||||
import Form, { type FormConfig } from "../util/Form.svelte"
|
||||
import { get_endpoint } from "../lib/PixeldrainAPI.mjs";
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
|
||||
let form = {
|
||||
name: "register",
|
||||
let form: FormConfig = {
|
||||
fields: [
|
||||
{
|
||||
name: "username",
|
||||
@@ -34,6 +31,7 @@ let form = {
|
||||
on_submit: async fields => {
|
||||
if (fields.password !== fields.password2) {
|
||||
return {
|
||||
success: false,
|
||||
error_json: {
|
||||
value: "password_verification_failed",
|
||||
message: "Password verification failed. Please enter the same " +
|
||||
@@ -48,33 +46,17 @@ let form = {
|
||||
form.append("password", fields.password)
|
||||
|
||||
const resp = await fetch(
|
||||
window.api_endpoint+"/user/register",
|
||||
get_endpoint()+"/user/register",
|
||||
{ method: "POST", body: form }
|
||||
);
|
||||
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
|
||||
// credentials
|
||||
|
||||
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()}
|
||||
return {
|
||||
success: true,
|
||||
message: "Account registration successful. Please check your inbox for an e-mail verification link"
|
||||
}
|
||||
let jresp = await login_resp.json()
|
||||
|
||||
dispatch("login", {key: jresp.auth_key})
|
||||
|
||||
return {success: true, message: "Successfully registered a new account"}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
36
svelte/src/login/Router.svelte
Normal file
36
svelte/src/login/Router.svelte
Normal 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 />
|
@@ -3,6 +3,7 @@ import { onMount } from "svelte";
|
||||
import CopyButton from "../layout/CopyButton.svelte";
|
||||
import Form from "./../util/Form.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_deny = false
|
||||
@@ -28,16 +29,13 @@ let account_settings = {
|
||||
please check your spam box too. Leave the field empty to remove
|
||||
your current e-mail address from your account`,
|
||||
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",
|
||||
label: "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",
|
||||
label: "New password again",
|
||||
@@ -67,7 +65,6 @@ let account_settings = {
|
||||
|
||||
const form = new FormData()
|
||||
form.append("email", fields.email)
|
||||
form.append("password_old", fields.password_old)
|
||||
form.append("password_new", fields.password_new1)
|
||||
form.append("username", fields.username)
|
||||
|
||||
@@ -114,14 +111,29 @@ let delete_account = {
|
||||
name: "description",
|
||||
label: "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
|
||||
scheduled for deletion in seven days. If you log back in to your
|
||||
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
|
||||
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,
|
||||
@@ -146,6 +158,11 @@ let delete_account = {
|
||||
<Form config={account_settings}></Form>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Two-factor authentication</legend>
|
||||
<OtpSetup/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Affiliate settings</legend>
|
||||
<Form config={affiliate_settings}></Form>
|
||||
|
@@ -37,7 +37,6 @@ const save_embed = async () => {
|
||||
onMount(() => {
|
||||
embed_domains = window.user.file_embed_domains
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<LoadingIndicator loading={loading}/>
|
||||
|
169
svelte/src/user_home/OTPSetup.svelte
Normal file
169
svelte/src/user_home/OTPSetup.svelte
Normal 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>
|
@@ -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 Spinner from "./Spinner.svelte";
|
||||
import type { GenericResponse } from "../lib/PixeldrainAPI.mjs";
|
||||
|
||||
|
||||
export let config = {
|
||||
fields: [
|
||||
{
|
||||
name: "",
|
||||
label: "",
|
||||
type: "",
|
||||
default_value: "",
|
||||
binding: null,
|
||||
}
|
||||
],
|
||||
submit_label: "",
|
||||
submit_red: false,
|
||||
on_submit: async field_values => {},
|
||||
}
|
||||
export let config: FormConfig
|
||||
|
||||
onMount(() => {
|
||||
config.fields.forEach(field => {
|
||||
@@ -28,15 +40,9 @@ onMount(() => {
|
||||
|
||||
let loading = false
|
||||
let submitted = false
|
||||
let submit_result = {
|
||||
success: false,
|
||||
message: "",
|
||||
messages: null,
|
||||
}
|
||||
let submit_result: SubmitResult
|
||||
|
||||
let form_elem
|
||||
|
||||
let submit = async (event) => {
|
||||
const submit = async (event: SubmitEvent) => {
|
||||
loading = true
|
||||
event.preventDefault()
|
||||
|
||||
@@ -51,15 +57,13 @@ let submit = async (event) => {
|
||||
}
|
||||
} else if (field.type === "description") {
|
||||
field_values[field.name] = ""
|
||||
} else if (field.type === "captcha") {
|
||||
field_values[field.name] = form_elem.getElementsByClassName("g-recaptcha-response")[0].value
|
||||
} else {
|
||||
field_values[field.name] = field.binding.value
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
submitted = true
|
||||
@@ -67,7 +71,8 @@ let submit = async (event) => {
|
||||
loading = false
|
||||
return false
|
||||
}
|
||||
let field_label = (field) => {
|
||||
|
||||
const field_label = (field: string) => {
|
||||
let label = ""
|
||||
config.fields.forEach(val => {
|
||||
if (val.name === field) {
|
||||
@@ -76,7 +81,8 @@ let field_label = (field) => {
|
||||
})
|
||||
return label
|
||||
}
|
||||
let handle_errors = (response) => {
|
||||
|
||||
const handle_errors = (response: GenericResponse) => {
|
||||
console.log(response)
|
||||
let result = {success: false, message: "", messages: null}
|
||||
|
||||
@@ -86,18 +92,18 @@ let handle_errors = (response) => {
|
||||
response.errors.forEach(err => {
|
||||
if (err.value === "string_out_of_range") {
|
||||
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
|
||||
${err.extra.max_len} characters. Current length:
|
||||
${err.extra.len}`
|
||||
)
|
||||
} else if (err.value === "field_contains_illegal_character") {
|
||||
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") {
|
||||
result.messages.push(
|
||||
`${field_label(err.extra.field)} is required`
|
||||
`${field_label(<string>err.extra.field)} is required`
|
||||
)
|
||||
} else {
|
||||
result.messages.push(err.message)
|
||||
@@ -111,8 +117,8 @@ let handle_errors = (response) => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<form method="POST" on:submit={submit} bind:this={form_elem}>
|
||||
{#if submitted}
|
||||
<form method="POST" on:submit={submit}>
|
||||
{#if submitted && submit_result !== undefined}
|
||||
{#if submit_result.messages}
|
||||
<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/>
|
||||
@@ -129,7 +135,6 @@ let handle_errors = (response) => {
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
||||
<div class="form">
|
||||
{#each config.fields as field}
|
||||
{#if field.type !== "description"}
|
||||
@@ -141,8 +146,10 @@ let handle_errors = (response) => {
|
||||
id="input_{field.name}"
|
||||
name="{field.name}"
|
||||
value="{field.default_value}"
|
||||
pattern={field.pattern}
|
||||
type="text"
|
||||
class="form_input"/>
|
||||
class="form_input"
|
||||
/>
|
||||
{:else if field.type === "text_area"}
|
||||
<textarea bind:this={field.binding}
|
||||
id="input_{field.name}"
|
||||
@@ -155,58 +162,79 @@ let handle_errors = (response) => {
|
||||
id="input_{field.name}"
|
||||
name="{field.name}"
|
||||
value="{field.default_value}"
|
||||
pattern={field.pattern}
|
||||
type="number"
|
||||
class="form_input"/>
|
||||
class="form_input"
|
||||
/>
|
||||
{:else if field.type === "decimal"}
|
||||
<input bind:this={field.binding}
|
||||
id="input_{field.name}"
|
||||
name="{field.name}"
|
||||
value="{field.default_value}"
|
||||
pattern={field.pattern}
|
||||
type="number"
|
||||
step="0.1"
|
||||
class="form_input"/>
|
||||
class="form_input"
|
||||
/>
|
||||
{:else if field.type === "datetime-local"}
|
||||
<input bind:this={field.binding}
|
||||
id="input_{field.name}"
|
||||
name="{field.name}"
|
||||
value="{field.default_value}"
|
||||
pattern={field.pattern}
|
||||
type="datetime-local"
|
||||
class="form_input"/>
|
||||
class="form_input"
|
||||
/>
|
||||
{:else if field.type === "username"}
|
||||
<input bind:this={field.binding}
|
||||
id="input_{field.name}"
|
||||
name="{field.name}"
|
||||
value="{field.default_value}"
|
||||
pattern={field.pattern}
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
class="form_input"/>
|
||||
class="form_input"
|
||||
/>
|
||||
{:else if field.type === "email"}
|
||||
<input bind:this={field.binding}
|
||||
id="input_{field.name}"
|
||||
name="{field.name}"
|
||||
value="{field.default_value}"
|
||||
pattern={field.pattern}
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
class="form_input"/>
|
||||
class="form_input"
|
||||
/>
|
||||
{:else if field.type === "current_password"}
|
||||
<input bind:this={field.binding}
|
||||
id="input_{field.name}"
|
||||
name="{field.name}"
|
||||
value="{field.default_value}"
|
||||
pattern={field.pattern}
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="form_input"/>
|
||||
class="form_input"
|
||||
/>
|
||||
{:else if field.type === "new_password"}
|
||||
<input bind:this={field.binding}
|
||||
id="input_{field.name}"
|
||||
name="{field.name}"
|
||||
value="{field.default_value}"
|
||||
pattern={field.pattern}
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="form_input"/>
|
||||
{:else if field.type === "captcha"}
|
||||
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
|
||||
<div class="g-recaptcha" data-theme="dark" data-sitekey="{field.captcha_site_key}"></div>
|
||||
class="form_input"
|
||||
/>
|
||||
{:else if field.type === "totp"}
|
||||
<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"}
|
||||
<div>
|
||||
{#each field.radio_values as val}
|
||||
|
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"fornaxian.tech/log"
|
||||
"fornaxian.tech/pixeldrain_api_client/pixelapi"
|
||||
@@ -70,185 +69,6 @@ func (wc *WebController) serveLogout(
|
||||
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) {
|
||||
f = Form{
|
||||
Name: "password_reset",
|
||||
|
@@ -156,10 +156,9 @@ func New(r *httprouter.Router, prefix string, conf Config) (wc *WebController) {
|
||||
{GET, "speedtest" /* */, wc.serveTemplate("speedtest", handlerOpts{})},
|
||||
|
||||
// User account pages
|
||||
{GET, "register" /* */, wc.serveForm(wc.registerForm, handlerOpts{NoEmbed: true})},
|
||||
{PST, "register" /* */, wc.serveForm(wc.registerForm, handlerOpts{NoEmbed: true})},
|
||||
{GET, "login" /* */, wc.serveForm(wc.loginForm, handlerOpts{NoEmbed: true})},
|
||||
{PST, "login" /* */, wc.serveForm(wc.loginForm, handlerOpts{NoEmbed: true})},
|
||||
{GET, "login" /* */, wc.serveTemplate("login", handlerOpts{NoEmbed: true})},
|
||||
{GET, "register" /* */, wc.serveTemplate("login", handlerOpts{NoEmbed: true})},
|
||||
|
||||
{GET, "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})},
|
||||
|
Reference in New Issue
Block a user