menu refactor + web account switcher (#7307)

* add account switcher, migrate context menus to overflow menu system, update version filter controls, only pad instance icons

* app account switcher, improve some context menus, theme stuff

* prepr

* handle reauthentication

* fix a couple sign in issues, admin page cleanup, fix org status badges, fix official account badge, open in local keybind, publish plus icons

* rename generic menus to ButtonMenu (& types)

* fix hydration issue w/ dropdowns + middleware error
This commit is contained in:
Prospector
2026-08-27 00:35:34 +00:00
committed by GitHub
parent 3439a43880
commit b68a199814
226 changed files with 6585 additions and 4078 deletions
@@ -0,0 +1,38 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id, active, session_id, expires\n FROM modrinth_users\n ORDER BY rowid\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "active",
"ordinal": 1,
"type_info": "Integer"
},
{
"name": "session_id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "expires",
"ordinal": 3,
"type_info": "Integer"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "c7ade4d5f2b4b910b8d3a100d7285ac08744fc65c686cc185c1af97ea7f0738a"
}
+77 -16
View File
@@ -1,6 +1,9 @@
use crate::state::ModrinthCredentials;
use crate::state::{FeatureFlag, ModrinthCredentials, Settings};
use serde::Deserialize;
const LOCALHOST_LOGIN_URL: &str = "http://localhost:3000/auth/sign-in";
const LOCALHOST_SIGNUP_URL: &str = "http://localhost:3000/auth/sign-up";
#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ModrinthAuthFlow {
@@ -9,11 +12,23 @@ pub enum ModrinthAuthFlow {
}
#[tracing::instrument]
pub fn authenticate_begin_flow(flow: ModrinthAuthFlow) -> &'static str {
match flow {
ModrinthAuthFlow::SignIn => crate::state::get_login_url(),
ModrinthAuthFlow::SignUp => crate::state::get_signup_url(),
}
pub async fn authenticate_begin_flow(
flow: ModrinthAuthFlow,
) -> crate::Result<&'static str> {
let state = crate::State::get().await?;
let settings = Settings::get(&state.pool).await?;
let use_localhost = settings
.feature_flags
.get(&FeatureFlag::LocalhostSignIn)
.copied()
.unwrap_or(false);
Ok(match (use_localhost, flow) {
(true, ModrinthAuthFlow::SignIn) => LOCALHOST_LOGIN_URL,
(true, ModrinthAuthFlow::SignUp) => LOCALHOST_SIGNUP_URL,
(false, ModrinthAuthFlow::SignIn) => crate::state::get_login_url(),
(false, ModrinthAuthFlow::SignUp) => crate::state::get_signup_url(),
})
}
#[tracing::instrument]
@@ -39,11 +54,7 @@ pub async fn authenticate_finish_flow(
);
}
state.friends_socket.disconnect().await?;
state
.friends_socket
.connect(&state.pool, &state.api_semaphore, &state.process_manager)
.await?;
reconnect_friends(&state).await?;
Ok(creds)
}
@@ -51,16 +62,66 @@ pub async fn authenticate_finish_flow(
#[tracing::instrument]
pub async fn logout() -> crate::Result<()> {
let state = crate::State::get().await?;
let current = ModrinthCredentials::get_active(&state.pool).await?;
if let Some(current) = current {
ModrinthCredentials::remove(&current.user_id, &state.pool).await?;
}
ModrinthCredentials::deactivate_all(&state.pool).await?;
state.friends_socket.disconnect().await?;
Ok(())
}
#[tracing::instrument]
pub async fn get_all() -> crate::Result<Vec<ModrinthCredentials>> {
let state = crate::State::get().await?;
ModrinthCredentials::get_all(&state.pool).await
}
#[tracing::instrument]
pub async fn set_active(user_id: &str) -> crate::Result<()> {
let state = crate::State::get().await?;
let users = ModrinthCredentials::get_all(&state.pool).await?;
let Some(mut creds) =
users.into_iter().find(|creds| creds.user_id == user_id)
else {
return Err(crate::ErrorKind::OtherError(format!(
"Tried to activate nonexistent Modrinth user with ID {user_id}"
))
.as_error());
};
creds.active = true;
creds.upsert(&state.pool).await?;
reconnect_friends(&state).await?;
Ok(())
}
#[tracing::instrument]
pub async fn remove_user(user_id: &str) -> crate::Result<()> {
let state = crate::State::get().await?;
let current = ModrinthCredentials::get_active(&state.pool).await?;
ModrinthCredentials::remove(user_id, &state.pool).await?;
if current.is_some_and(|creds| creds.user_id == user_id) {
state.friends_socket.disconnect().await?;
}
Ok(())
}
async fn reconnect_friends(state: &crate::State) -> crate::Result<()> {
if let Err(error) = state.friends_socket.disconnect().await {
tracing::warn!("Failed to disconnect friends socket: {error}");
}
if let Err(error) = state
.friends_socket
.connect(&state.pool, &state.api_semaphore, &state.process_manager)
.await
{
tracing::warn!("Failed to reconnect friends socket: {error}");
}
Ok(())
}
#[tracing::instrument]
pub async fn get_credentials() -> crate::Result<Option<ModrinthCredentials>> {
let state = crate::State::get().await?;
+1 -5
View File
@@ -591,11 +591,7 @@ async fn censor_support_text(
mut text: String,
state: &State,
) -> crate::Result<String> {
for credentials in ModrinthCredentials::get_all(&state.pool)
.await?
.into_iter()
.map(|credentials| credentials.1)
{
for credentials in ModrinthCredentials::get_all(&state.pool).await? {
replace_nonempty(
&mut text,
&credentials.session,
+32 -30
View File
@@ -1,8 +1,6 @@
use crate::state::{CacheBehaviour, CachedEntry};
use crate::util::fetch::{FetchSemaphore, fetch_advanced};
use chrono::{DateTime, Duration, TimeZone, Utc};
use dashmap::DashMap;
use futures::TryStreamExt;
use reqwest::Method;
use serde::{Deserialize, Serialize};
@@ -90,34 +88,30 @@ impl ModrinthCredentials {
pub async fn get_all(
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
) -> crate::Result<DashMap<String, Self>> {
) -> crate::Result<Vec<Self>> {
let res = sqlx::query!(
"
SELECT
id, active, session_id, expires
FROM modrinth_users
ORDER BY rowid
"
)
.fetch(exec)
.try_fold(DashMap::new(), |acc, x| {
acc.insert(
x.id.clone(),
Self {
session: x.session_id,
expires: Utc
.timestamp_opt(x.expires, 0)
.single()
.unwrap_or_else(Utc::now),
user_id: x.id,
active: x.active == 1,
},
);
async move { Ok(acc) }
})
.fetch_all(exec)
.await?;
Ok(res)
Ok(res
.into_iter()
.map(|x| Self {
session: x.session_id,
expires: Utc
.timestamp_opt(x.expires, 0)
.single()
.unwrap_or_else(Utc::now),
user_id: x.id,
active: x.active == 1,
})
.collect())
}
pub async fn upsert(
@@ -127,14 +121,7 @@ impl ModrinthCredentials {
let expires = self.expires.timestamp();
if self.active {
sqlx::query!(
"
UPDATE modrinth_users
SET active = FALSE
"
)
.execute(exec)
.await?;
Self::deactivate_all(exec).await?;
}
sqlx::query!(
@@ -157,6 +144,21 @@ impl ModrinthCredentials {
Ok(())
}
pub async fn deactivate_all(
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
) -> crate::Result<()> {
sqlx::query!(
"
UPDATE modrinth_users
SET active = FALSE
"
)
.execute(exec)
.await?;
Ok(())
}
pub async fn remove(
user_id: &str,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
@@ -177,7 +179,7 @@ impl ModrinthCredentials {
let state = crate::State::get().await?;
let all = Self::get_all(&state.pool).await?;
let user_ids = all.into_iter().map(|x| x.0).collect::<Vec<_>>();
let user_ids = all.into_iter().map(|x| x.user_id).collect::<Vec<_>>();
CachedEntry::get_user_many(
&user_ids.iter().map(|x| &**x).collect::<Vec<_>>(),
+1
View File
@@ -72,6 +72,7 @@ pub enum FeatureFlag {
FriendsOfflineCollapsed,
FriendsPendingCollapsed,
DismissedPhotosensitivityFilterWarning,
LocalhostSignIn,
}
impl Settings {