add: finish ui rewrite
This commit is contained in:
parent
e9846016e6
commit
5dec98d698
119 changed files with 8776 additions and 9350 deletions
|
@ -1,9 +0,0 @@
|
|||
{% extends "root.html" %} {% block body %}
|
||||
<main class="flex flex-col gap-2" style="max-width: 25rem">
|
||||
<h2 class="w-full text-center">{% block title %}{% endblock %}</h2>
|
||||
<div class="card w-full flex flex-col gap-4 justify-center align-center">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
{% block footer %}{% endblock %}
|
||||
</main>
|
||||
{% endblock %}
|
16
crates/app/src/public/html/auth/base.lisp
Normal file
16
crates/app/src/public/html/auth/base.lisp
Normal file
|
@ -0,0 +1,16 @@
|
|||
(text "{% extends \"root.html\" %} {% block body %}")
|
||||
(main
|
||||
("class" "flex flex-col gap-2")
|
||||
("style" "max-width: 25rem")
|
||||
(h2
|
||||
("class" "w-full text-center")
|
||||
; block for title
|
||||
(text "{% block title %}{% endblock %}"))
|
||||
(div
|
||||
("class" "card w-full flex flex-col gap-4 justify-center align-center")
|
||||
; block for actual page content
|
||||
(text "{% block content %}{% endblock %}"))
|
||||
; small footer block (for switching context)
|
||||
(text "{% block footer %}{% endblock %}"))
|
||||
|
||||
(text "{% endblock %}")
|
|
@ -1,75 +0,0 @@
|
|||
{% extends "auth/base.html" %} {% block head %}
|
||||
<title>Connection</title>
|
||||
{% endblock %} {% block title %}Connection{% endblock %} {% block content %}
|
||||
<div class="w-full flex-col gap-2" id="status"><b>Working...</b></div>
|
||||
|
||||
{% if connection_type == "Spotify" and user and user.connections.Spotify and
|
||||
config.connections.spotify_client_id %}
|
||||
<script>
|
||||
setTimeout(async () => {
|
||||
const code = new URLSearchParams(window.location.search).get("code");
|
||||
const client_id = "{{ config.connections.spotify_client_id }}";
|
||||
const verifier = "{{ user.connections.Spotify[0].data.verifier }}";
|
||||
|
||||
if (!code) {
|
||||
alert("Connection failed (did not get code from Spotify)");
|
||||
return;
|
||||
}
|
||||
|
||||
const [token, refresh_token, expires_in] = await trigger(
|
||||
"spotify::get_token",
|
||||
[client_id, verifier, code],
|
||||
);
|
||||
|
||||
const profile = await trigger("spotify::profile", [token]);
|
||||
|
||||
const { message } = await trigger("connections::push_con_data", [
|
||||
"Spotify",
|
||||
{
|
||||
token,
|
||||
refresh_token,
|
||||
expires_in: expires_in.toString(),
|
||||
name: profile.display_name,
|
||||
url: profile.external_urls.spotify,
|
||||
},
|
||||
]);
|
||||
|
||||
document.getElementById("status").innerHTML =
|
||||
`<b>${message}.</b> You can now close this tab.`;
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = "/settings#/connections";
|
||||
}, 500);
|
||||
}, 150);
|
||||
</script>
|
||||
{% elif connection_type == "LastFm" and user and user.connections.LastFm and
|
||||
config.connections.last_fm_key %}
|
||||
<script>
|
||||
setTimeout(async () => {
|
||||
const token = new URLSearchParams(window.location.search).get("token");
|
||||
const api_key = "{{ config.connections.last_fm_key }}";
|
||||
|
||||
if (!token) {
|
||||
alert("Connection failed (did not get token from Last.fm)");
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await trigger("last_fm::get_session", [token]);
|
||||
const { message } = await trigger("connections::push_con_data", [
|
||||
"LastFm",
|
||||
{
|
||||
session_token: res.session.key,
|
||||
name: res.session.name,
|
||||
url: `https://last.fm/user/${res.session.name}`,
|
||||
},
|
||||
]);
|
||||
|
||||
document.getElementById("status").innerHTML =
|
||||
`<b>${message}.</b> You can now close this tab.`;
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = "/settings#/connections";
|
||||
}, 500);
|
||||
}, 1000);
|
||||
</script>
|
||||
{%- endif %} {% endblock %}
|
80
crates/app/src/public/html/auth/connection.lisp
Normal file
80
crates/app/src/public/html/auth/connection.lisp
Normal file
|
@ -0,0 +1,80 @@
|
|||
(text "{% extends \"auth/base.html\" %} {% block head %}")
|
||||
(title
|
||||
(text "Connection"))
|
||||
|
||||
(text "{% endblock %} {% block title %}Connection{% endblock %} {% block content %}")
|
||||
(div
|
||||
("class" "w-full flex-col gap-2")
|
||||
("id" "status")
|
||||
; display loading text because we have to wait for the data to update on remote
|
||||
(b
|
||||
(text "Working...")))
|
||||
|
||||
(text "{% if connection_type == \"Spotify\" and user and user.connections.Spotify and config.connections.spotify_client_id %}")
|
||||
(script
|
||||
(text "setTimeout(async () => {
|
||||
const code = new URLSearchParams(window.location.search).get(\"code\");
|
||||
const client_id = \"{{ config.connections.spotify_client_id }}\";
|
||||
const verifier = \"{{ user.connections.Spotify[0].data.verifier }}\";
|
||||
|
||||
if (!code) {
|
||||
alert(\"Connection failed (did not get code from Spotify)\");
|
||||
return;
|
||||
}
|
||||
|
||||
const [token, refresh_token, expires_in] = await trigger(
|
||||
\"spotify::get_token\",
|
||||
[client_id, verifier, code],
|
||||
);
|
||||
|
||||
const profile = await trigger(\"spotify::profile\", [token]);
|
||||
|
||||
const { message } = await trigger(\"connections::push_con_data\", [
|
||||
\"Spotify\",
|
||||
{
|
||||
token,
|
||||
refresh_token,
|
||||
expires_in: expires_in.toString(),
|
||||
name: profile.display_name,
|
||||
url: profile.external_urls.spotify,
|
||||
},
|
||||
]);
|
||||
|
||||
document.getElementById(\"status\").innerHTML =
|
||||
`<b>${message}.</b> You can now close this tab.`;
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = \"/settings#/connections\";
|
||||
}, 500);
|
||||
}, 150);"))
|
||||
|
||||
(text "{% elif connection_type == \"LastFm\" and user and user.connections.LastFm and config.connections.last_fm_key %}")
|
||||
(script
|
||||
(text "setTimeout(async () => {
|
||||
const token = new URLSearchParams(window.location.search).get(\"token\");
|
||||
const api_key = \"{{ config.connections.last_fm_key }}\";
|
||||
|
||||
if (!token) {
|
||||
alert(\"Connection failed (did not get token from Last.fm)\");
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await trigger(\"last_fm::get_session\", [token]);
|
||||
const { message } = await trigger(\"connections::push_con_data\", [
|
||||
\"LastFm\",
|
||||
{
|
||||
session_token: res.session.key,
|
||||
name: res.session.name,
|
||||
url: `https://last.fm/user/${res.session.name}`,
|
||||
},
|
||||
]);
|
||||
|
||||
document.getElementById(\"status\").innerHTML =
|
||||
`<b>${message}.</b> You can now close this tab.`;
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = \"/settings#/connections\";
|
||||
}, 500);
|
||||
}, 1000);"))
|
||||
|
||||
(text "{%- endif %} {% endblock %}")
|
|
@ -1,103 +0,0 @@
|
|||
{% extends "auth/base.html" %} {% block head %}
|
||||
<title>Login</title>
|
||||
{% endblock %} {% block title %}Login{% endblock %} {% block content %}
|
||||
<form class="w-full flex flex-col gap-4" onsubmit="login(event)">
|
||||
<div id="flow_1" style="display: contents">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="username"><b>Username</b></label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="username"
|
||||
required
|
||||
name="username"
|
||||
id="username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="username"><b>Password</b></label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="password"
|
||||
required
|
||||
name="password"
|
||||
id="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="flow_2" style="display: none">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="totp"><b>TOTP code</b></label>
|
||||
<input type="text" placeholder="totp code" name="totp" id="totp" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
let flow_page = 1;
|
||||
|
||||
function next_page() {
|
||||
document.getElementById(`flow_${flow_page}`).style.display = "none";
|
||||
flow_page += 1;
|
||||
document.getElementById(`flow_${flow_page}`).style.display = "contents";
|
||||
}
|
||||
|
||||
async function login(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (flow_page === 1) {
|
||||
// check if we need TOTP
|
||||
const res = await (
|
||||
await fetch(
|
||||
`/api/v1/auth/user/${e.target.username.value}/totp/check`,
|
||||
)
|
||||
).json();
|
||||
|
||||
trigger("atto::toast", [res.ok ? "success" : "error", res.message]);
|
||||
|
||||
if (res.ok && res.payload) {
|
||||
// user exists AND totp is required
|
||||
return next_page();
|
||||
}
|
||||
}
|
||||
|
||||
fetch("/api/v1/auth/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: e.target.username.value,
|
||||
password: e.target.password.value,
|
||||
totp: e.target.totp.value,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger("atto::toast", [
|
||||
res.ok ? "success" : "error",
|
||||
res.message,
|
||||
]);
|
||||
|
||||
if (res.ok) {
|
||||
// update tokens
|
||||
const new_tokens = ns("me").LOGIN_ACCOUNT_TOKENS;
|
||||
new_tokens[e.target.username.value] = res.message;
|
||||
trigger("me::set_login_account_tokens", [new_tokens]);
|
||||
|
||||
// redirect
|
||||
setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %} {% block footer %}
|
||||
<span class="small w-full text-center"
|
||||
>Or, <a href="/auth/register">register</a></span
|
||||
>
|
||||
{% endblock %}
|
121
crates/app/src/public/html/auth/login.lisp
Normal file
121
crates/app/src/public/html/auth/login.lisp
Normal file
|
@ -0,0 +1,121 @@
|
|||
(text "{% extends \"auth/base.html\" %} {% block head %}")
|
||||
(title
|
||||
(text "Login"))
|
||||
|
||||
(text "{% endblock %} {% block title %}Login{% endblock %} {% block content %}")
|
||||
(form
|
||||
("class" "w-full flex flex-col gap-4")
|
||||
("onsubmit" "login(event)")
|
||||
(div
|
||||
("id" "flow_1")
|
||||
("style" "display: contents")
|
||||
(div
|
||||
("class" "flex flex-col gap-1")
|
||||
(label
|
||||
("for" "username")
|
||||
(b
|
||||
(text "Username")))
|
||||
(input
|
||||
("type" "text")
|
||||
("placeholder" "username")
|
||||
("required" "")
|
||||
("name" "username")
|
||||
("id" "username")))
|
||||
(div
|
||||
("class" "flex flex-col gap-1")
|
||||
(label
|
||||
("for" "username")
|
||||
(b
|
||||
(text "Password")))
|
||||
(input
|
||||
("type" "password")
|
||||
("placeholder" "password")
|
||||
("required" "")
|
||||
("name" "password")
|
||||
("id" "password"))))
|
||||
(div
|
||||
("id" "flow_2")
|
||||
("style" "display: none")
|
||||
(div
|
||||
("class" "flex flex-col gap-1")
|
||||
(label
|
||||
("for" "totp")
|
||||
(b
|
||||
(text "TOTP code")))
|
||||
(input
|
||||
("type" "text")
|
||||
("placeholder" "totp code")
|
||||
("name" "totp")
|
||||
("id" "totp"))))
|
||||
(button
|
||||
(text "Submit")))
|
||||
|
||||
(script
|
||||
(text "let flow_page = 1;
|
||||
|
||||
function next_page() {
|
||||
document.getElementById(`flow_${flow_page}`).style.display = \"none\";
|
||||
flow_page += 1;
|
||||
document.getElementById(`flow_${flow_page}`).style.display = \"contents\";
|
||||
}
|
||||
|
||||
async function login(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (flow_page === 1) {
|
||||
// check if we need TOTP
|
||||
const res = await (
|
||||
await fetch(
|
||||
`/api/v1/auth/user/${e.target.username.value}/totp/check`,
|
||||
)
|
||||
).json();
|
||||
|
||||
trigger(\"atto::toast\", [res.ok ? \"success\" : \"error\", res.message]);
|
||||
|
||||
if (res.ok && res.payload) {
|
||||
// user exists AND totp is required
|
||||
return next_page();
|
||||
}
|
||||
}
|
||||
|
||||
fetch(\"/api/v1/auth/login\", {
|
||||
method: \"POST\",
|
||||
headers: {
|
||||
\"Content-Type\": \"application/json\",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: e.target.username.value,
|
||||
password: e.target.password.value,
|
||||
totp: e.target.totp.value,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
|
||||
if (res.ok) {
|
||||
// update tokens
|
||||
const new_tokens = ns(\"me\").LOGIN_ACCOUNT_TOKENS;
|
||||
new_tokens[e.target.username.value] = res.message;
|
||||
trigger(\"me::set_login_account_tokens\", [new_tokens]);
|
||||
|
||||
// redirect
|
||||
setTimeout(() => {
|
||||
window.location.href = \"/\";
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
}"))
|
||||
|
||||
(text "{% endblock %} {% block footer %}")
|
||||
(span
|
||||
("class" "small w-full text-center")
|
||||
(text "Or, ")
|
||||
(a
|
||||
("href" "/auth/register")
|
||||
(text "register")))
|
||||
|
||||
(text "{% endblock %}")
|
|
@ -1,120 +0,0 @@
|
|||
{% extends "auth/base.html" %} {% block head %}
|
||||
<title>Register</title>
|
||||
{% endblock %} {% block title %}Register{% endblock %} {% block content %}
|
||||
<script
|
||||
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
|
||||
defer
|
||||
></script>
|
||||
|
||||
<form class="w-full flex flex-col gap-4" onsubmit="register(event)">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="username"><b>Username</b></label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="username"
|
||||
required
|
||||
name="username"
|
||||
id="username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="username"><b>Password</b></label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="password"
|
||||
required
|
||||
name="password"
|
||||
id="password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="card-nest w-full">
|
||||
<div class="card small flex items-center gap-2">
|
||||
{{ icon "scroll-text" }}
|
||||
<b>Policies</b>
|
||||
</div>
|
||||
|
||||
<div class="card secondary flex flex-col gap-2">
|
||||
<span>By continuing, you agree to the following policies:</span>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ config.policies.terms_of_service }}"
|
||||
>Terms of service</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="{{ config.policies.privacy }}">Privacy policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="policy_consent"
|
||||
id="policy_consent"
|
||||
class="w-content"
|
||||
required
|
||||
/>
|
||||
<label for="policy_consent">I agree</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="cf-turnstile"
|
||||
data-sitekey="{{ config.turnstile.site_key }}"
|
||||
></div>
|
||||
|
||||
<hr />
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
async function register(e) {
|
||||
e.preventDefault();
|
||||
await trigger("atto::debounce", ["users::create"]);
|
||||
fetch("/api/v1/auth/register", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: e.target.username.value,
|
||||
password: e.target.password.value,
|
||||
policy_consent: e.target.policy_consent.checked,
|
||||
captcha_response: e.target.querySelector(
|
||||
"[name=cf-turnstile-response]",
|
||||
).value,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger("atto::toast", [
|
||||
res.ok ? "success" : "error",
|
||||
res.message,
|
||||
]);
|
||||
|
||||
if (res.ok) {
|
||||
// update tokens
|
||||
const new_tokens = ns("me").LOGIN_ACCOUNT_TOKENS;
|
||||
new_tokens[e.target.username.value] = res.message;
|
||||
trigger("me::set_login_account_tokens", [new_tokens]);
|
||||
|
||||
// redirect
|
||||
setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %} {% block footer %}
|
||||
<span class="small w-full text-center"
|
||||
>Or, <a href="/auth/login">login</a></span
|
||||
>
|
||||
{% endblock %}
|
123
crates/app/src/public/html/auth/register.lisp
Normal file
123
crates/app/src/public/html/auth/register.lisp
Normal file
|
@ -0,0 +1,123 @@
|
|||
(text "{% extends \"auth/base.html\" %} {% block head %}")
|
||||
(title
|
||||
(text "Register"))
|
||||
|
||||
(text "{% endblock %} {% block title %}Register{% endblock %} {% block content %}")
|
||||
(script
|
||||
("src" "https://challenges.cloudflare.com/turnstile/v0/api.js")
|
||||
("defer" ""))
|
||||
|
||||
(form
|
||||
("class" "w-full flex flex-col gap-4")
|
||||
("onsubmit" "register(event)")
|
||||
(div
|
||||
("class" "flex flex-col gap-1")
|
||||
(label
|
||||
("for" "username")
|
||||
(b
|
||||
(text "Username")))
|
||||
(input
|
||||
("type" "text")
|
||||
("placeholder" "username")
|
||||
("required" "")
|
||||
("name" "username")
|
||||
("id" "username")))
|
||||
(div
|
||||
("class" "flex flex-col gap-1")
|
||||
(label
|
||||
("for" "username")
|
||||
(b
|
||||
(text "Password")))
|
||||
(input
|
||||
("type" "password")
|
||||
("placeholder" "password")
|
||||
("required" "")
|
||||
("name" "password")
|
||||
("id" "password")))
|
||||
(hr)
|
||||
(div
|
||||
("class" "card-nest w-full")
|
||||
(div
|
||||
("class" "card small flex items-center gap-2")
|
||||
(text "{{ icon \"scroll-text\" }}")
|
||||
(b
|
||||
(text "Policies")))
|
||||
(div
|
||||
("class" "card secondary flex flex-col gap-2")
|
||||
(span
|
||||
(text "By continuing, you agree to the following policies:"))
|
||||
(ul
|
||||
(li
|
||||
(a
|
||||
("href" "{{ config.policies.terms_of_service }}")
|
||||
(text "Terms of service")))
|
||||
(li
|
||||
(a
|
||||
("href" "{{ config.policies.privacy }}")
|
||||
(text "Privacy policy"))))
|
||||
(div
|
||||
("class" "flex gap-2")
|
||||
(input
|
||||
("type" "checkbox")
|
||||
("name" "policy_consent")
|
||||
("id" "policy_consent")
|
||||
("class" "w-content")
|
||||
("required" ""))
|
||||
(label
|
||||
("for" "policy_consent")
|
||||
(text "I agree")))))
|
||||
(div
|
||||
("class" "cf-turnstile")
|
||||
("data-sitekey" "{{ config.turnstile.site_key }}"))
|
||||
(hr)
|
||||
(button
|
||||
(text "Submit")))
|
||||
|
||||
(script
|
||||
(text "async function register(e) {
|
||||
e.preventDefault();
|
||||
await trigger(\"atto::debounce\", [\"users::create\"]);
|
||||
fetch(\"/api/v1/auth/register\", {
|
||||
method: \"POST\",
|
||||
headers: {
|
||||
\"Content-Type\": \"application/json\",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: e.target.username.value,
|
||||
password: e.target.password.value,
|
||||
policy_consent: e.target.policy_consent.checked,
|
||||
captcha_response: e.target.querySelector(
|
||||
\"[name=cf-turnstile-response]\",
|
||||
).value,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
|
||||
if (res.ok) {
|
||||
// update tokens
|
||||
const new_tokens = ns(\"me\").LOGIN_ACCOUNT_TOKENS;
|
||||
new_tokens[e.target.username.value] = res.message;
|
||||
trigger(\"me::set_login_account_tokens\", [new_tokens]);
|
||||
|
||||
// redirect
|
||||
setTimeout(() => {
|
||||
window.location.href = \"/\";
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
}"))
|
||||
|
||||
(text "{% endblock %} {% block footer %}")
|
||||
(span
|
||||
("class" "small w-full text-center")
|
||||
(text "Or, ")
|
||||
(a
|
||||
("href" "/auth/login")
|
||||
(text "login")))
|
||||
|
||||
(text "{% endblock %}")
|
Loading…
Add table
Add a link
Reference in a new issue