feat: limit concurrent sessions per user with active session management (CW-7169)

This commit is contained in:
Vishnu Narayanan
2026-06-02 13:11:58 +05:30
committed by Vishnu Narayanan
parent 3eed8905cc
commit 13ad505bbb
22 changed files with 703 additions and 2 deletions
+9
View File
@@ -43,6 +43,15 @@ export const login = async ({
mfaToken: error.response.data.mfa_token,
};
}
if (
error.response?.status === 409 &&
error.response?.data?.sessions_limit_reached
) {
return {
sessionsLimitReached: true,
sessions: error.response.data.sessions,
};
}
const loginError = new Error(parseAPIErrorResponse(error));
loginError.errorCode = error.response?.data?.error_code;
throw loginError;
+67 -1
View File
@@ -17,6 +17,7 @@ import Spinner from 'shared/components/Spinner.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
import MfaVerification from 'dashboard/components/auth/MfaVerification.vue';
import SessionLimitOverlay from 'dashboard/components/auth/SessionLimitOverlay.vue';
const ERROR_MESSAGES = {
'no-account-found': 'LOGIN.OAUTH.NO_ACCOUNT_FOUND',
@@ -36,6 +37,7 @@ export default {
NextButton,
SimpleDivider,
MfaVerification,
SessionLimitOverlay,
Icon,
},
props: {
@@ -68,6 +70,8 @@ export default {
error: '',
mfaRequired: false,
mfaToken: null,
sessionsLimitReached: false,
limitedSessions: [],
};
},
validations() {
@@ -182,6 +186,14 @@ export default {
return;
}
// Check if sessions limit reached
if (result?.sessionsLimitReached) {
this.loginApi.showLoading = false;
this.sessionsLimitReached = true;
this.limitedSessions = result.sessions;
return;
}
this.handleImpersonation();
this.showAlertMessage(this.$t('LOGIN.API.SUCCESS_MESSAGE'));
})
@@ -224,6 +236,50 @@ export default {
this.mfaToken = null;
this.credentials.password = '';
},
retryLoginWithParams(extraParams) {
const credentials = {
email: this.email
? decodeURIComponent(this.email)
: this.credentials.email,
password: this.credentials.password,
sso_auth_token: this.ssoAuthToken,
ssoAccountId: this.ssoAccountId,
ssoConversationId: this.ssoConversationId,
...extraParams,
};
this.sessionsLimitReached = false;
this.limitedSessions = [];
this.loginApi.showLoading = true;
login(credentials)
.then(result => {
if (result?.sessionsLimitReached) {
this.loginApi.showLoading = false;
this.sessionsLimitReached = true;
this.limitedSessions = result.sessions;
return;
}
this.handleImpersonation();
this.showAlertMessage(this.$t('LOGIN.API.SUCCESS_MESSAGE'));
})
.catch(response => {
this.loginApi.hasErrored = true;
this.showAlertMessage(
response?.message || this.$t('LOGIN.API.UNAUTH')
);
});
},
handleSessionRevoke(sessionId) {
this.retryLoginWithParams({ revoke_session_id: sessionId });
},
handleSessionRevokeAll() {
this.retryLoginWithParams({ revoke_all_sessions: true });
},
handleSessionLimitCancel() {
this.sessionsLimitReached = false;
this.limitedSessions = [];
this.credentials.password = '';
},
},
};
</script>
@@ -255,8 +311,18 @@ export default {
</p>
</section>
<!-- Session Limit Section -->
<section v-if="sessionsLimitReached" class="mt-11">
<SessionLimitOverlay
:sessions="limitedSessions"
@revoke="handleSessionRevoke"
@revoke-all="handleSessionRevokeAll"
@cancel="handleSessionLimitCancel"
/>
</section>
<!-- MFA Verification Section -->
<section v-if="mfaRequired" class="mt-11">
<section v-else-if="mfaRequired" class="mt-11">
<MfaVerification
:mfa-token="mfaToken"
@verified="handleMfaVerified"