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",
|
"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: {
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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>
|
||||||
|
|
||||||
|
@@ -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
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>
|
<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>
|
||||||
|
@@ -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>
|
||||||
|
|
||||||
|
@@ -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>
|
||||||
|
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 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>
|
||||||
|
@@ -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}/>
|
||||||
|
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 { 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}
|
||||||
|
@@ -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",
|
||||||
|
@@ -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})},
|
||||||
|
Reference in New Issue
Block a user