add: user account switcher
This commit is contained in:
parent
48e0b02198
commit
20aae5570b
13 changed files with 172 additions and 34 deletions
|
@ -17,6 +17,8 @@ version = "1.0.0"
|
||||||
"general:action.back" = "Back"
|
"general:action.back" = "Back"
|
||||||
"general:action.report" = "Report"
|
"general:action.report" = "Report"
|
||||||
"general:action.manage" = "Manage"
|
"general:action.manage" = "Manage"
|
||||||
|
"general:action.add_account" = "Add account"
|
||||||
|
"general:action.switch_account" = "Switch account"
|
||||||
"general:label.mod" = "Mod"
|
"general:label.mod" = "Mod"
|
||||||
"general:label.file_report" = "File report"
|
"general:label.file_report" = "File report"
|
||||||
"general:label.account_banned" = "Account banned"
|
"general:label.account_banned" = "Account banned"
|
||||||
|
|
|
@ -48,6 +48,12 @@
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (res.ok) {
|
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(() => {
|
setTimeout(() => {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}, 150);
|
}, 150);
|
||||||
|
|
|
@ -100,6 +100,12 @@
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (res.ok) {
|
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(() => {
|
setTimeout(() => {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}, 150);
|
}, 150);
|
||||||
|
|
|
@ -107,6 +107,11 @@ show_lhs=true) -%}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="title"></div>
|
<div class="title"></div>
|
||||||
|
<button onclick="trigger('me::switch_account')">
|
||||||
|
{{ icon "ellipsis" }}
|
||||||
|
<span>{{ text "general:action.switch_account" }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="red" onclick="trigger('me::logout')">
|
<button class="red" onclick="trigger('me::logout')">
|
||||||
{{ icon "log-out" }}
|
{{ icon "log-out" }}
|
||||||
<span>{{ text "auth:action.logout" }}</span>
|
<span>{{ text "auth:action.logout" }}</span>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% import "macros.html" as macros %} {% extends "profile/base.html" %} {% block
|
{% import "macros.html" as macros %} {% extends "profile/base.html" %} {% block
|
||||||
content %} {% if config.town_square %}
|
content %} {% if config.town_square and is_self %}
|
||||||
<div class="card-nest">
|
<div class="card-nest">
|
||||||
<div class="card small flex flex-col">
|
<div class="card small flex flex-col">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|
|
@ -109,6 +109,12 @@
|
||||||
atto["hooks::check_reactions"]();
|
atto["hooks::check_reactions"]();
|
||||||
atto["hooks::tabs"]();
|
atto["hooks::tabs"]();
|
||||||
atto["hooks::partial_embeds"]();
|
atto["hooks::partial_embeds"]();
|
||||||
|
|
||||||
|
if (document.getElementById("tokens")) {
|
||||||
|
trigger("me::render_token_picker", [
|
||||||
|
document.getElementById("tokens"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -257,5 +263,38 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
{% if user %}
|
||||||
|
<dialog id="tokens_dialog">
|
||||||
|
<div class="inner flex flex-col gap-2">
|
||||||
|
<form
|
||||||
|
class="flex gap-2 flex-col"
|
||||||
|
onsubmit="event.preventDefault()"
|
||||||
|
>
|
||||||
|
<div id="tokens" style="display: contents"></div>
|
||||||
|
|
||||||
|
<a href="/auth/login" class="button" data-turbo="false">
|
||||||
|
{{ icon "plus" }}
|
||||||
|
<span>{{ text "general:action.add_account" }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div></div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
class="quaternary"
|
||||||
|
onclick="document.getElementById('tokens_dialog').close()"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{{ icon "check" }}
|
||||||
|
<span>{{ text "dialog:action.okay" }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
(() => {
|
(() => {
|
||||||
const self = reg_ns("me");
|
const self = reg_ns("me");
|
||||||
|
|
||||||
|
self.LOGIN_ACCOUNT_TOKENS = JSON.parse(
|
||||||
|
window.localStorage.getItem("atto:login_account_tokens") || "{}",
|
||||||
|
);
|
||||||
|
|
||||||
self.define("logout", async () => {
|
self.define("logout", async () => {
|
||||||
if (
|
if (
|
||||||
!(await trigger("atto::confirm", [
|
!(await trigger("atto::confirm", [
|
||||||
|
@ -162,4 +166,48 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// token switcher
|
||||||
|
self.define(
|
||||||
|
"set_login_account_tokens",
|
||||||
|
({ $ }, value) => {
|
||||||
|
$.LOGIN_ACCOUNT_TOKENS = value;
|
||||||
|
window.localStorage.setItem(
|
||||||
|
"atto:login_account_tokens",
|
||||||
|
JSON.stringify(value),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
["object"],
|
||||||
|
);
|
||||||
|
|
||||||
|
self.define("login", ({ $ }, username) => {
|
||||||
|
const token = self.LOGIN_ACCOUNT_TOKENS[username];
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = `/api/v1/auth/token?token=${token}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.define("render_token_picker", ({ $ }, element) => {
|
||||||
|
element.innerHTML = "";
|
||||||
|
for (const token of Object.entries($.LOGIN_ACCOUNT_TOKENS)) {
|
||||||
|
element.innerHTML += `<button class="quaternary w-full justify-start" onclick="trigger('me::login', ['${token[0]}'])">
|
||||||
|
<img
|
||||||
|
title="${token[0]}'s avatar"
|
||||||
|
src="/api/v1/auth/profile/${token[0]}/avatar?selector_type=username"
|
||||||
|
alt="Avatar image"
|
||||||
|
class="avatar"
|
||||||
|
style="--size: 24px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span>${token[0]}</span>
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.define("switch_account", () => {
|
||||||
|
document.getElementById("tokens_dialog").showModal();
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -9,11 +9,13 @@ use crate::{
|
||||||
model::{ApiReturn, Error, auth::User},
|
model::{ApiReturn, Error, auth::User},
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension, Json,
|
extract::Query,
|
||||||
http::{HeaderMap, HeaderValue},
|
http::{HeaderMap, HeaderValue},
|
||||||
response::IntoResponse,
|
response::{IntoResponse, Redirect},
|
||||||
|
Extension, Json,
|
||||||
};
|
};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
|
use serde::Deserialize;
|
||||||
use tetratto_shared::hash::hash;
|
use tetratto_shared::hash::hash;
|
||||||
|
|
||||||
use cf_turnstile::{SiteVerifyRequest, TurnstileClient};
|
use cf_turnstile::{SiteVerifyRequest, TurnstileClient};
|
||||||
|
@ -21,23 +23,23 @@ use cf_turnstile::{SiteVerifyRequest, TurnstileClient};
|
||||||
/// `/api/v1/auth/register`
|
/// `/api/v1/auth/register`
|
||||||
pub async fn register_request(
|
pub async fn register_request(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
jar: CookieJar,
|
// jar: CookieJar,
|
||||||
Extension(data): Extension<State>,
|
Extension(data): Extension<State>,
|
||||||
Json(props): Json<RegisterProps>,
|
Json(props): Json<RegisterProps>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let data = &(data.read().await).0;
|
let data = &(data.read().await).0;
|
||||||
let user = get_user_from_token!(jar, data);
|
// let user = get_user_from_token!(jar, data);
|
||||||
|
|
||||||
if user.is_some() {
|
// if user.is_some() {
|
||||||
return (
|
// return (
|
||||||
None,
|
// None,
|
||||||
Json(ApiReturn {
|
// Json(ApiReturn {
|
||||||
ok: false,
|
// ok: false,
|
||||||
message: Error::AlreadyAuthenticated.to_string(),
|
// message: Error::AlreadyAuthenticated.to_string(),
|
||||||
payload: (),
|
// payload: (),
|
||||||
}),
|
// }),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
// get real ip
|
// get real ip
|
||||||
let real_ip = headers
|
let real_ip = headers
|
||||||
|
@ -93,7 +95,7 @@ pub async fn register_request(
|
||||||
)]),
|
)]),
|
||||||
Json(ApiReturn {
|
Json(ApiReturn {
|
||||||
ok: true,
|
ok: true,
|
||||||
message: "User created".to_string(),
|
message: initial_token,
|
||||||
payload: (),
|
payload: (),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -104,16 +106,16 @@ pub async fn register_request(
|
||||||
/// `/api/v1/auth/login`
|
/// `/api/v1/auth/login`
|
||||||
pub async fn login_request(
|
pub async fn login_request(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
jar: CookieJar,
|
// jar: CookieJar,
|
||||||
Extension(data): Extension<State>,
|
Extension(data): Extension<State>,
|
||||||
Json(props): Json<LoginProps>,
|
Json(props): Json<LoginProps>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let data = &(data.read().await).0;
|
let data = &(data.read().await).0;
|
||||||
let user = get_user_from_token!(jar, data);
|
// let user = get_user_from_token!(jar, data);
|
||||||
|
|
||||||
if user.is_some() {
|
// if user.is_some() {
|
||||||
return (None, Json(Error::AlreadyAuthenticated.into()));
|
// return (None, Json(Error::AlreadyAuthenticated.into()));
|
||||||
}
|
// }
|
||||||
|
|
||||||
// get real ip
|
// get real ip
|
||||||
let real_ip = headers
|
let real_ip = headers
|
||||||
|
@ -211,3 +213,32 @@ pub async fn logout_request(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct SetTokenQuery {
|
||||||
|
#[serde(default)]
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the current user token.
|
||||||
|
pub async fn set_token_request(Query(props): Query<SetTokenQuery>) -> impl IntoResponse {
|
||||||
|
(
|
||||||
|
{
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
|
||||||
|
headers.insert(
|
||||||
|
"Set-Cookie",
|
||||||
|
format!(
|
||||||
|
"__Secure-atto-token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}",
|
||||||
|
props.token,
|
||||||
|
60* 60 * 24 * 365
|
||||||
|
)
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
headers
|
||||||
|
},
|
||||||
|
Redirect::to("/"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -94,6 +94,7 @@ pub fn routes() -> Router {
|
||||||
.route("/auth/register", post(auth::register_request))
|
.route("/auth/register", post(auth::register_request))
|
||||||
.route("/auth/login", post(auth::login_request))
|
.route("/auth/login", post(auth::login_request))
|
||||||
.route("/auth/logout", post(auth::logout_request))
|
.route("/auth/logout", post(auth::logout_request))
|
||||||
|
.route("/auth/token", get(auth::set_token_request))
|
||||||
.route(
|
.route(
|
||||||
"/auth/upload/avatar",
|
"/auth/upload/avatar",
|
||||||
post(auth::images::upload_avatar_request),
|
post(auth::images::upload_avatar_request),
|
||||||
|
|
|
@ -109,7 +109,7 @@ pub struct LangFileQuery {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the current language
|
/// Set the current language.
|
||||||
pub async fn set_langfile_request(Query(props): Query<LangFileQuery>) -> impl IntoResponse {
|
pub async fn set_langfile_request(Query(props): Query<LangFileQuery>) -> impl IntoResponse {
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{State, assets::initial_context, get_lang, get_user_from_token};
|
use crate::{State, assets::initial_context, get_lang, get_user_from_token};
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension,
|
Extension,
|
||||||
response::{Html, IntoResponse, Redirect},
|
response::{Html, IntoResponse},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
|
|
||||||
|
@ -10,14 +10,14 @@ pub async fn login_request(jar: CookieJar, Extension(data): Extension<State>) ->
|
||||||
let data = data.read().await;
|
let data = data.read().await;
|
||||||
let user = get_user_from_token!(jar, data.0);
|
let user = get_user_from_token!(jar, data.0);
|
||||||
|
|
||||||
if user.is_some() {
|
// if user.is_some() {
|
||||||
return Err(Redirect::to("/"));
|
// return Err(Redirect::to("/"));
|
||||||
}
|
// }
|
||||||
|
|
||||||
let lang = get_lang!(jar, data.0);
|
let lang = get_lang!(jar, data.0);
|
||||||
let context = initial_context(&data.0.0, lang, &user).await;
|
let context = initial_context(&data.0.0, lang, &user).await;
|
||||||
|
|
||||||
Ok(Html(data.1.render("auth/login.html", &context).unwrap()))
|
Html(data.1.render("auth/login.html", &context).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `/auth/register`
|
/// `/auth/register`
|
||||||
|
@ -28,12 +28,12 @@ pub async fn register_request(
|
||||||
let data = data.read().await;
|
let data = data.read().await;
|
||||||
let user = get_user_from_token!(jar, data.0);
|
let user = get_user_from_token!(jar, data.0);
|
||||||
|
|
||||||
if user.is_some() {
|
// if user.is_some() {
|
||||||
return Err(Redirect::to("/"));
|
// return Err(Redirect::to("/"));
|
||||||
}
|
// }
|
||||||
|
|
||||||
let lang = get_lang!(jar, data.0);
|
let lang = get_lang!(jar, data.0);
|
||||||
let context = initial_context(&data.0.0, lang, &user).await;
|
let context = initial_context(&data.0.0, lang, &user).await;
|
||||||
|
|
||||||
Ok(Html(data.1.render("auth/register.html", &context).unwrap()))
|
Html(data.1.render("auth/register.html", &context).unwrap())
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,7 +133,7 @@ impl DataManager {
|
||||||
|
|
||||||
let res = query_rows!(
|
let res = query_rows!(
|
||||||
&conn,
|
&conn,
|
||||||
"SELECT * FROM communities ORDER BY likes DESC LIMIT 12",
|
"SELECT * FROM communities ORDER BY member_count DESC LIMIT 12",
|
||||||
empty,
|
empty,
|
||||||
|x| { Self::get_community_from_row(x) }
|
|x| { Self::get_community_from_row(x) }
|
||||||
);
|
);
|
||||||
|
|
|
@ -107,8 +107,8 @@ impl DataManager {
|
||||||
.create_notification(Notification::new(
|
.create_notification(Notification::new(
|
||||||
"Your community has received a like!".to_string(),
|
"Your community has received a like!".to_string(),
|
||||||
format!(
|
format!(
|
||||||
"[@{}](/api/v1/auth/profile/find/{}) has liked your community!",
|
"[@{}](/api/v1/auth/profile/find/{}) has liked your [community](/api/v1/communities/find/{})!",
|
||||||
user.username, user.id
|
user.username, user.id, community.id
|
||||||
),
|
),
|
||||||
community.owner,
|
community.owner,
|
||||||
))
|
))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue