Pipe session_limit to frontend

This commit is contained in:
Eric Eastwood
2026-04-17 18:22:32 -05:00
parent d1edf64dda
commit 6dce03e27d
5 changed files with 88 additions and 14 deletions
@@ -59,6 +59,9 @@ pub struct SiteConfig {
/// Experimental plan management iframe URI.
plan_management_iframe_uri: Option<String>,
/// Limits on the number of application sessions that each user can have
session_limit: Option<SessionLimitConfig>,
}
#[derive(SimpleObject)]
@@ -79,6 +82,25 @@ pub enum CaptchaService {
HCaptcha,
}
#[derive(SimpleObject)]
pub struct SessionLimitConfig {
pub soft_limit: u64,
pub hard_limit: u64,
pub hard_limit_eviction: bool,
}
impl SessionLimitConfig {
/// Create a new [`SessionLimitConfig`] from the data model
/// [`mas_data_model::SessionLimitConfig`].
pub fn new(data_model: &mas_data_model::SessionLimitConfig) -> Self {
Self {
soft_limit: data_model.soft_limit.get(),
hard_limit: data_model.hard_limit.get(),
hard_limit_eviction: data_model.hard_limit_eviction,
}
}
}
#[ComplexObject]
impl SiteConfig {
/// The ID of the site configuration.
@@ -106,6 +128,10 @@ impl SiteConfig {
minimum_password_complexity: data_model.minimum_password_complexity,
login_with_email_allowed: data_model.login_with_email_allowed,
plan_management_iframe_uri: data_model.plan_management_iframe_uri.clone(),
session_limit: data_model
.session_limit
.as_ref()
.map(SessionLimitConfig::new),
}
}
}
+10
View File
@@ -1402,6 +1402,12 @@ A client session, either compat or OAuth 2.0
"""
union Session = CompatSession | Oauth2Session
type SessionLimitConfig {
softLimit: Int!
hardLimit: Int!
hardLimitEviction: Boolean!
}
"""
The state of a session
"""
@@ -1762,6 +1768,10 @@ type SiteConfig implements Node {
"""
planManagementIframeUri: String
"""
Limits on the number of application sessions that each user can have
"""
sessionLimit: SessionLimitConfig
"""
The ID of the site configuration.
"""
id: ID!
+3 -3
View File
@@ -52,7 +52,7 @@ type Documents = {
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n ...AccountDeleteButton_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n accountDeactivationAllowed\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n ...AccountDeleteButton_siteConfig\n }\n }\n": typeof types.UserProfileDocument,
"\n query PlanManagementTab {\n siteConfig {\n planManagementIframeUri\n }\n }\n": typeof types.PlanManagementTabDocument,
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": typeof types.BrowserSessionListDocument,
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n\n # Get the total count of active app sessions before any filtering\n unfilteredAppSessions: appSessions(first: 1, state: ACTIVE) {\n totalCount\n }\n }\n }\n }\n": typeof types.SessionsOverviewDocument,
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n\n # Get the total count of active app sessions before any filtering\n unfilteredAppSessions: appSessions(first: 1, state: ACTIVE) {\n totalCount\n }\n }\n }\n\n siteConfig {\n sessionLimit {\n softLimit\n }\n }\n }\n": typeof types.SessionsOverviewDocument,
"\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": typeof types.AppSessionsListDocument,
"\n query CurrentUserGreeting {\n viewer {\n __typename\n ... on User {\n ...UserGreeting_user\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n planManagementIframeUri\n }\n }\n": typeof types.CurrentUserGreetingDocument,
"\n query OAuth2Client($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n": typeof types.OAuth2ClientDocument,
@@ -109,7 +109,7 @@ const documents: Documents = {
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n ...AccountDeleteButton_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n accountDeactivationAllowed\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n ...AccountDeleteButton_siteConfig\n }\n }\n": types.UserProfileDocument,
"\n query PlanManagementTab {\n siteConfig {\n planManagementIframeUri\n }\n }\n": types.PlanManagementTabDocument,
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": types.BrowserSessionListDocument,
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n\n # Get the total count of active app sessions before any filtering\n unfilteredAppSessions: appSessions(first: 1, state: ACTIVE) {\n totalCount\n }\n }\n }\n }\n": types.SessionsOverviewDocument,
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n\n # Get the total count of active app sessions before any filtering\n unfilteredAppSessions: appSessions(first: 1, state: ACTIVE) {\n totalCount\n }\n }\n }\n\n siteConfig {\n sessionLimit {\n softLimit\n }\n }\n }\n": types.SessionsOverviewDocument,
"\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": types.AppSessionsListDocument,
"\n query CurrentUserGreeting {\n viewer {\n __typename\n ... on User {\n ...UserGreeting_user\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n planManagementIframeUri\n }\n }\n": types.CurrentUserGreetingDocument,
"\n query OAuth2Client($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n": types.OAuth2ClientDocument,
@@ -280,7 +280,7 @@ export function graphql(source: "\n query BrowserSessionList(\n $first: Int\
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n\n # Get the total count of active app sessions before any filtering\n unfilteredAppSessions: appSessions(first: 1, state: ACTIVE) {\n totalCount\n }\n }\n }\n }\n"): typeof import('./graphql').SessionsOverviewDocument;
export function graphql(source: "\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n\n # Get the total count of active app sessions before any filtering\n unfilteredAppSessions: appSessions(first: 1, state: ACTIVE) {\n totalCount\n }\n }\n }\n\n siteConfig {\n sessionLimit {\n softLimit\n }\n }\n }\n"): typeof import('./graphql').SessionsOverviewDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
+16 -2
View File
@@ -1078,6 +1078,13 @@ export type ResendRecoveryEmailStatus =
/** A client session, either compat or OAuth 2.0 */
export type Session = CompatSession | Oauth2Session;
export type SessionLimitConfig = {
__typename?: 'SessionLimitConfig';
hardLimit: Scalars['Int']['output'];
hardLimitEviction: Scalars['Boolean']['output'];
softLimit: Scalars['Int']['output'];
};
/** The state of a session */
export type SessionState =
/** The session is active. */
@@ -1302,6 +1309,8 @@ export type SiteConfig = Node & {
policyUri?: Maybe<Scalars['Url']['output']>;
/** The server name of the homeserver. */
serverName: Scalars['String']['output'];
/** Limits on the number of application sessions that each user can have */
sessionLimit?: Maybe<SessionLimitConfig>;
/** The URL to the terms of service. */
tosUri?: Maybe<Scalars['Url']['output']>;
};
@@ -1899,7 +1908,7 @@ export type SessionsOverviewQuery = { __typename?: 'Query', viewer:
{ __typename: 'User', id: string, unfilteredAppSessions: { __typename?: 'AppSessionConnection', totalCount: number } }
& { ' $fragmentRefs'?: { 'BrowserSessionsOverview_UserFragment': BrowserSessionsOverview_UserFragment } }
)
};
, siteConfig: { __typename?: 'SiteConfig', sessionLimit?: { __typename?: 'SessionLimitConfig', softLimit: number } | null } };
export type AppSessionsListQueryVariables = Exact<{
before?: InputMaybe<Scalars['String']['input']>;
@@ -2710,6 +2719,11 @@ export const SessionsOverviewDocument = new TypedDocumentString(`
}
}
}
siteConfig {
sessionLimit {
softLimit
}
}
}
fragment BrowserSessionsOverview_user on User {
id
@@ -3401,7 +3415,7 @@ export const mockBrowserSessionListQuery = (resolver: GraphQLResponseResolver<Br
* mockSessionsOverviewQuery(
* ({ query, variables }) => {
* return HttpResponse.json({
* data: { viewer }
* data: { viewer, siteConfig }
* })
* },
* requestOptions
@@ -43,6 +43,12 @@ const QUERY = graphql(/* GraphQL */ `
}
}
}
siteConfig {
sessionLimit {
softLimit
}
}
}
`);
@@ -141,9 +147,10 @@ function Sessions(): React.ReactElement {
const { t } = useTranslation();
const { inactive, pagination } = Route.useLoaderDeps();
const {
data: { viewer: overviewViewer },
data: { viewer: overviewViewer, siteConfig },
} = useSuspenseQuery(query);
if (overviewViewer.__typename !== "User") throw notFound();
const { sessionLimit } = siteConfig;
const { data } = useSuspenseQuery(listQuery(pagination, inactive));
if (data.viewer.__typename !== "User") throw notFound();
@@ -159,16 +166,32 @@ function Sessions(): React.ReactElement {
const edges = [...appSessions.edges].reverse();
// By default, we just show a "X devices" header
let deviceHeaderText = t("frontend.user_sessions_overview.num_devices_header", {
num_devices: appSessions.totalCount,
});
let deviceHeaderText = t(
"frontend.user_sessions_overview.num_devices_header",
{
num_devices: appSessions.totalCount,
},
);
// But if we're showing a filtered down view, we want to explain how many devices you
// filtered down to and how many total unfilterd devices there are total.
if (overviewViewer.unfilteredAppSessions.totalCount != appSessions.totalCount) {
deviceHeaderText = t("frontend.user_sessions_overview.num_devices_filtered_header", {
filtered_count: appSessions.totalCount,
total_count: overviewViewer.unfilteredAppSessions.totalCount,
});
if (
overviewViewer.unfilteredAppSessions.totalCount != appSessions.totalCount
) {
deviceHeaderText = t(
"frontend.user_sessions_overview.num_devices_filtered_header",
{
filtered_count: appSessions.totalCount,
total_count: overviewViewer.unfilteredAppSessions.totalCount,
},
);
}
// TODO
let todoSessionLimit = null;
if (sessionLimit) {
todoSessionLimit = (
<div>TODO: Limited to {sessionLimit.softLimit} devices</div>
);
}
return (
@@ -177,6 +200,7 @@ function Sessions(): React.ReactElement {
<BrowserSessionsOverview user={overviewViewer} />
<H4>{deviceHeaderText}</H4>
{todoSessionLimit}
<Separator kind="section" />
<div className="flex gap-2 justify-start items-center">
<Filter