tenants
This commit is contained in:
@@ -539,6 +539,7 @@ pub mod schemas {
|
|||||||
pub access_token: String,
|
pub access_token: String,
|
||||||
pub token_type: String,
|
pub token_type: String,
|
||||||
pub expires_in: i64,
|
pub expires_in: i64,
|
||||||
|
pub tenant: TenantSnippet,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, ToSchema)]
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
@@ -547,9 +548,15 @@ pub mod schemas {
|
|||||||
pub slug: String,
|
pub slug: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct TenantSnippet {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub slug: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, ToSchema)]
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
pub struct TenantSelectionResponse {
|
pub struct TenantSelectionResponse {
|
||||||
pub selection_token: String,
|
pub access_token: String,
|
||||||
pub tenants: Vec<TenantSummary>,
|
pub tenants: Vec<TenantSummary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ pub struct LoginResponse {
|
|||||||
pub access_token: String,
|
pub access_token: String,
|
||||||
pub token_type: String,
|
pub token_type: String,
|
||||||
pub expires_in: i64,
|
pub expires_in: i64,
|
||||||
|
pub tenant: TenantSnippet,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -51,12 +52,23 @@ pub struct TenantSummary {
|
|||||||
pub slug: String,
|
pub slug: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct TenantSnippet {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub slug: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct TenantSelectionResponse {
|
pub struct TenantSelectionResponse {
|
||||||
pub selection_token: String,
|
pub access_token: String,
|
||||||
pub tenants: Vec<TenantSummary>,
|
pub tenants: Vec<TenantSummary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct TenantListResponse {
|
||||||
|
pub tenants: Vec<TenantSnippet>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct TenantSelectionRequest {
|
pub struct TenantSelectionRequest {
|
||||||
pub tenant_id: Uuid,
|
pub tenant_id: Uuid,
|
||||||
@@ -121,7 +133,7 @@ pub async fn login(
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let response = Json(TenantSelectionResponse {
|
let response = Json(TenantSelectionResponse {
|
||||||
selection_token,
|
access_token: selection_token,
|
||||||
tenants,
|
tenants,
|
||||||
})
|
})
|
||||||
.into_response();
|
.into_response();
|
||||||
@@ -250,6 +262,38 @@ pub async fn me(user: AuthenticatedUser) -> Json<AuthenticatedUser> {
|
|||||||
Json(user)
|
Json(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_tenants(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
auth: Option<TypedHeader<Authorization<Bearer>>>,
|
||||||
|
) -> AppResult<Json<TenantListResponse>> {
|
||||||
|
let bearer = auth.ok_or_else(AppError::unauthorized)?;
|
||||||
|
let token = bearer.token();
|
||||||
|
|
||||||
|
let user_id = match state.jwt.verify_token(token) {
|
||||||
|
Ok(claims) => claims.sub,
|
||||||
|
Err(_) => {
|
||||||
|
let claims = state
|
||||||
|
.jwt
|
||||||
|
.verify_tenant_selector_token(token)
|
||||||
|
.map_err(|_| AppError::unauthorized())?;
|
||||||
|
claims.sub
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut conn = state.db_unscoped()?;
|
||||||
|
|
||||||
|
let tenants = memberships_dsl::user_memberships
|
||||||
|
.inner_join(tenant_dsl::tenants)
|
||||||
|
.filter(memberships_dsl::user_id.eq(user_id))
|
||||||
|
.select((tenant_dsl::id, tenant_dsl::slug))
|
||||||
|
.load::<(Uuid, String)>(&mut conn)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, slug)| TenantSnippet { id, slug })
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Json(TenantListResponse { tenants }))
|
||||||
|
}
|
||||||
|
|
||||||
fn issue_session(
|
fn issue_session(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
conn: &mut PgConnection,
|
conn: &mut PgConnection,
|
||||||
@@ -262,6 +306,12 @@ fn issue_session(
|
|||||||
.generate_token(user.id, tenant_id, &user.username)
|
.generate_token(user.id, tenant_id, &user.username)
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
|
let tenant_slug: String = tenant_dsl::tenants
|
||||||
|
.find(tenant_id)
|
||||||
|
.select(tenant_dsl::slug)
|
||||||
|
.first(conn)
|
||||||
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
let refresh_value = generate_refresh_token();
|
let refresh_value = generate_refresh_token();
|
||||||
let refresh_hash = hash_refresh_token(&refresh_value);
|
let refresh_hash = hash_refresh_token(&refresh_value);
|
||||||
let refresh_expires_at = now + ChronoDuration::days(state.config.refresh_token_expiry_days);
|
let refresh_expires_at = now + ChronoDuration::days(state.config.refresh_token_expiry_days);
|
||||||
@@ -283,6 +333,10 @@ fn issue_session(
|
|||||||
access_token,
|
access_token,
|
||||||
token_type: "Bearer".to_string(),
|
token_type: "Bearer".to_string(),
|
||||||
expires_in: state.config.jwt_expiry_minutes * 60,
|
expires_in: state.config.jwt_expiry_minutes * 60,
|
||||||
|
tenant: TenantSnippet {
|
||||||
|
id: tenant_id,
|
||||||
|
slug: tenant_slug,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.into_response();
|
.into_response();
|
||||||
|
|
||||||
|
|||||||
@@ -411,10 +411,7 @@ pub async fn list_documents(
|
|||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.map(|s| s.to_owned());
|
.map(|s| s.to_owned());
|
||||||
|
|
||||||
let mut include_descendants = include_descendants.unwrap_or_else(|| folder_id.is_some());
|
let include_descendants = include_descendants.unwrap_or(true);
|
||||||
if search_text.is_some() || tags_param.is_some() || correspondents_param.is_some() {
|
|
||||||
include_descendants = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
match (folder_id, include_descendants) {
|
match (folder_id, include_descendants) {
|
||||||
(Some(folder_id), true) => {
|
(Some(folder_id), true) => {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ pub fn create_router(state: AppState) -> Router<()> {
|
|||||||
.route("/refresh", post(auth::refresh))
|
.route("/refresh", post(auth::refresh))
|
||||||
.route("/logout", post(auth::logout))
|
.route("/logout", post(auth::logout))
|
||||||
.route("/select-tenant", post(auth::select_tenant))
|
.route("/select-tenant", post(auth::select_tenant))
|
||||||
|
.route("/tenants", get(auth::list_tenants))
|
||||||
.route("/me", get(auth::me));
|
.route("/me", get(auth::me));
|
||||||
|
|
||||||
let documents_routes = Router::new()
|
let documents_routes = Router::new()
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ impl TestApp {
|
|||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct TenantSelectionResponse {
|
struct TenantSelectionResponse {
|
||||||
selection_token: String,
|
access_token: String,
|
||||||
tenants: Vec<TenantSummary>,
|
tenants: Vec<TenantSummary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +350,7 @@ impl TestApp {
|
|||||||
&SelectTenantPayload {
|
&SelectTenantPayload {
|
||||||
tenant_id: target_tenant,
|
tenant_id: target_tenant,
|
||||||
},
|
},
|
||||||
Some(&selection.selection_token),
|
Some(&selection.access_token),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -5,8 +5,8 @@ Unless noted otherwise, endpoints below require a valid `Authorization: Bearer <
|
|||||||
|
|
||||||
Authentication
|
Authentication
|
||||||
--------------
|
--------------
|
||||||
- POST /api/auth/login - Exchange username/password for an access token and refresh cookie (public).
|
- POST /api/auth/login - Exchange username/password for an access token and refresh cookie (public). Returns the active tenant as `{ tenant: { id, slug } }`. When multiple tenants are available, the response contains an `access_token` (tenant-selector token) and tenant list instead.
|
||||||
- POST /api/auth/refresh - Rotate the refresh cookie and return a new access token (public, requires refresh cookie).
|
- POST /api/auth/refresh - Rotate the refresh cookie and return a new access token (public, requires refresh cookie). Response also includes the current tenant `{ tenant: { id, slug } }`.
|
||||||
- POST /api/auth/logout - Revoke the caller's refresh tokens and clear the cookie.
|
- POST /api/auth/logout - Revoke the caller's refresh tokens and clear the cookie.
|
||||||
- GET /api/auth/me - Return the authenticated principal payload.
|
- GET /api/auth/me - Return the authenticated principal payload.
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ Health
|
|||||||
|
|
||||||
Documents
|
Documents
|
||||||
---------
|
---------
|
||||||
- GET /api/documents - List or search documents. Optional filters: `folder_id` (defaults to root when omitted), `include_deleted`, `include_descendants` (defaults to true when a `folder_id` is provided and no other override is supplied), `query` (Quickwit full-text), `tags` (comma-separated tag UUIDs), and `correspondents` (comma-separated correspondent UUIDs). Each entry includes tags, correspondent assignments, and current version info.
|
- GET /api/documents - List or search documents. Optional filters: `folder_id` (defaults to root when omitted), `include_deleted`, `include_descendants` (defaults to true unless explicitly set to `false` without filters), `query` (Quickwit full-text), `tags` (comma-separated tag UUIDs), and `correspondents` (comma-separated correspondent UUIDs). Each entry includes tags, correspondent assignments, and current version info.
|
||||||
- GET /api/documents/check?checksum=<sha256> - Lightweight checksum preflight. Returns `exists=false` when no document with the supplied SHA-256 checksum is present; otherwise returns `exists=true` plus the current document metadata.
|
- GET /api/documents/check?checksum=<sha256> - Lightweight checksum preflight. Returns `exists=false` when no document with the supplied SHA-256 checksum is present; otherwise returns `exists=true` plus the current document metadata.
|
||||||
- POST /api/documents - Upload a document via multipart form-data. Required field: `file`. Optional fields: `title`, `folder_id`, JSON `metadata`, JSON array `tag_ids`, JSON array `correspondents` (each with `correspondent_id` and `role`), and `issued_at` (RFC3339). When `title` is supplied, the stored filename becomes `<title><original_extension>`. Include `skip_existing=true` to receive `204 No Content` instead of reusing a matching document.
|
- POST /api/documents - Upload a document via multipart form-data. Required field: `file`. Optional fields: `title`, `folder_id`, JSON `metadata`, JSON array `tag_ids`, JSON array `correspondents` (each with `correspondent_id` and `role`), and `issued_at` (RFC3339). When `title` is supplied, the stored filename becomes `<title><original_extension>`. Include `skip_existing=true` to receive `204 No Content` instead of reusing a matching document.
|
||||||
- POST /api/documents/bulk/move - Move multiple documents to a target folder.
|
- POST /api/documents/bulk/move - Move multiple documents to a target folder.
|
||||||
|
|||||||
+109
-10
@@ -63,6 +63,16 @@ const api = axios.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const STORED_TOKEN = window.localStorage.getItem('papercrate_token') || '';
|
const STORED_TOKEN = window.localStorage.getItem('papercrate_token') || '';
|
||||||
|
let STORED_TENANT = null;
|
||||||
|
try {
|
||||||
|
const rawTenant = window.localStorage.getItem('papercrate_tenant');
|
||||||
|
if (rawTenant) {
|
||||||
|
STORED_TENANT = JSON.parse(rawTenant);
|
||||||
|
}
|
||||||
|
} catch (
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
error
|
||||||
|
) {}
|
||||||
if (STORED_TOKEN) {
|
if (STORED_TOKEN) {
|
||||||
api.defaults.headers.common.Authorization = `Bearer ${STORED_TOKEN}`;
|
api.defaults.headers.common.Authorization = `Bearer ${STORED_TOKEN}`;
|
||||||
}
|
}
|
||||||
@@ -73,6 +83,8 @@ const initialAppState = {
|
|||||||
error: null,
|
error: null,
|
||||||
isRefreshing: false,
|
isRefreshing: false,
|
||||||
tenantSelection: null,
|
tenantSelection: null,
|
||||||
|
tenant: STORED_TENANT,
|
||||||
|
tenants: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const AppStateContext = React.createContext(null);
|
const AppStateContext = React.createContext(null);
|
||||||
@@ -81,7 +93,14 @@ const AppDispatchContext = React.createContext(null);
|
|||||||
const appStateReducer = (state, action) => {
|
const appStateReducer = (state, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'LOGIN_REQUEST':
|
case 'LOGIN_REQUEST':
|
||||||
return { ...state, status: 'authenticating', error: null, tenantSelection: null };
|
return {
|
||||||
|
...state,
|
||||||
|
status: 'authenticating',
|
||||||
|
error: null,
|
||||||
|
tenantSelection: null,
|
||||||
|
tenant: null,
|
||||||
|
tenants: [],
|
||||||
|
};
|
||||||
case 'LOGIN_SUCCESS':
|
case 'LOGIN_SUCCESS':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -89,6 +108,8 @@ const appStateReducer = (state, action) => {
|
|||||||
token: action.token,
|
token: action.token,
|
||||||
error: null,
|
error: null,
|
||||||
tenantSelection: null,
|
tenantSelection: null,
|
||||||
|
tenant: action.tenant || null,
|
||||||
|
tenants: state.tenants,
|
||||||
};
|
};
|
||||||
case 'LOGIN_FAILURE':
|
case 'LOGIN_FAILURE':
|
||||||
return {
|
return {
|
||||||
@@ -97,6 +118,8 @@ const appStateReducer = (state, action) => {
|
|||||||
error: action.error || null,
|
error: action.error || null,
|
||||||
isRefreshing: false,
|
isRefreshing: false,
|
||||||
tenantSelection: null,
|
tenantSelection: null,
|
||||||
|
tenant: null,
|
||||||
|
tenants: [],
|
||||||
};
|
};
|
||||||
case 'TENANT_SELECTION_REQUIRED':
|
case 'TENANT_SELECTION_REQUIRED':
|
||||||
return {
|
return {
|
||||||
@@ -108,6 +131,8 @@ const appStateReducer = (state, action) => {
|
|||||||
selectionToken: action.selectionToken,
|
selectionToken: action.selectionToken,
|
||||||
tenants: action.tenants,
|
tenants: action.tenants,
|
||||||
},
|
},
|
||||||
|
tenant: null,
|
||||||
|
tenants: [],
|
||||||
};
|
};
|
||||||
case 'CLEAR_TENANT_SELECTION':
|
case 'CLEAR_TENANT_SELECTION':
|
||||||
return {
|
return {
|
||||||
@@ -116,6 +141,8 @@ const appStateReducer = (state, action) => {
|
|||||||
error: null,
|
error: null,
|
||||||
isRefreshing: false,
|
isRefreshing: false,
|
||||||
tenantSelection: null,
|
tenantSelection: null,
|
||||||
|
tenant: null,
|
||||||
|
tenants: [],
|
||||||
};
|
};
|
||||||
case 'BOOTSTRAP_START':
|
case 'BOOTSTRAP_START':
|
||||||
return { ...state, status: 'bootstrapping', error: null };
|
return { ...state, status: 'bootstrapping', error: null };
|
||||||
@@ -132,6 +159,8 @@ const appStateReducer = (state, action) => {
|
|||||||
isRefreshing: false,
|
isRefreshing: false,
|
||||||
status: state.status === 'logged-out' ? 'authenticated' : state.status,
|
status: state.status === 'logged-out' ? 'authenticated' : state.status,
|
||||||
tenantSelection: null,
|
tenantSelection: null,
|
||||||
|
tenant: action.tenant || state.tenant || null,
|
||||||
|
tenants: state.tenants,
|
||||||
};
|
};
|
||||||
case 'TOKEN_REFRESH_FAILURE':
|
case 'TOKEN_REFRESH_FAILURE':
|
||||||
return {
|
return {
|
||||||
@@ -140,11 +169,26 @@ const appStateReducer = (state, action) => {
|
|||||||
error: action.error || null,
|
error: action.error || null,
|
||||||
isRefreshing: false,
|
isRefreshing: false,
|
||||||
tenantSelection: null,
|
tenantSelection: null,
|
||||||
|
tenant: null,
|
||||||
|
tenants: [],
|
||||||
};
|
};
|
||||||
case 'LOGOUT':
|
case 'LOGOUT':
|
||||||
return { status: 'logged-out', token: '', error: null, isRefreshing: false, tenantSelection: null };
|
return {
|
||||||
|
status: 'logged-out',
|
||||||
|
token: '',
|
||||||
|
error: null,
|
||||||
|
isRefreshing: false,
|
||||||
|
tenantSelection: null,
|
||||||
|
tenant: null,
|
||||||
|
tenants: [],
|
||||||
|
};
|
||||||
case 'RESET_ERROR':
|
case 'RESET_ERROR':
|
||||||
return { ...state, error: null };
|
return { ...state, error: null };
|
||||||
|
case 'SET_TENANTS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
tenants: Array.isArray(action.tenants) ? action.tenants : [],
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -164,6 +208,45 @@ const AppStateProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
}, [state.token]);
|
}, [state.token]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.tenant) {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem('papercrate_tenant', JSON.stringify(state.tenant));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to persist tenant info', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.localStorage.removeItem('papercrate_tenant');
|
||||||
|
}
|
||||||
|
}, [state.tenant]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let abort = false;
|
||||||
|
const loadTenants = async () => {
|
||||||
|
if (state.status !== 'authenticated' || !state.token) {
|
||||||
|
dispatch({ type: 'SET_TENANTS', tenants: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { data } = await api.get('/auth/tenants');
|
||||||
|
if (!abort) {
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_TENANTS',
|
||||||
|
tenants: Array.isArray(data?.tenants) ? data.tenants : [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!abort) {
|
||||||
|
console.warn('Failed to load tenant list', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadTenants();
|
||||||
|
return () => {
|
||||||
|
abort = true;
|
||||||
|
};
|
||||||
|
}, [state.status, state.token, dispatch]);
|
||||||
|
|
||||||
const stateValue = useMemo(() => state, [state]);
|
const stateValue = useMemo(() => state, [state]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -416,7 +499,11 @@ const AppLayout = () => {
|
|||||||
try {
|
try {
|
||||||
const { data } = await api.post('/auth/refresh');
|
const { data } = await api.post('/auth/refresh');
|
||||||
if (data?.access_token) {
|
if (data?.access_token) {
|
||||||
appDispatch({ type: 'TOKEN_REFRESH_SUCCESS', token: data.access_token });
|
appDispatch({
|
||||||
|
type: 'TOKEN_REFRESH_SUCCESS',
|
||||||
|
token: data.access_token,
|
||||||
|
tenant: data.tenant || null,
|
||||||
|
});
|
||||||
console.log('[Auth] Access token refreshed at', new Date().toISOString());
|
console.log('[Auth] Access token refreshed at', new Date().toISOString());
|
||||||
return data.access_token;
|
return data.access_token;
|
||||||
}
|
}
|
||||||
@@ -4196,10 +4283,10 @@ const AppLayout = () => {
|
|||||||
appDispatch({ type: 'LOGIN_REQUEST' });
|
appDispatch({ type: 'LOGIN_REQUEST' });
|
||||||
const { data } = await api.post('/auth/login', payload);
|
const { data } = await api.post('/auth/login', payload);
|
||||||
|
|
||||||
if (data?.selection_token && Array.isArray(data?.tenants)) {
|
if (data?.access_token && Array.isArray(data?.tenants)) {
|
||||||
appDispatch({
|
appDispatch({
|
||||||
type: 'TENANT_SELECTION_REQUIRED',
|
type: 'TENANT_SELECTION_REQUIRED',
|
||||||
selectionToken: data.selection_token,
|
selectionToken: data.access_token,
|
||||||
tenants: data.tenants,
|
tenants: data.tenants,
|
||||||
});
|
});
|
||||||
setStatusMessage('Select a tenant to continue.', 'info');
|
setStatusMessage('Select a tenant to continue.', 'info');
|
||||||
@@ -4210,7 +4297,11 @@ const AppLayout = () => {
|
|||||||
throw new Error('Invalid login response.');
|
throw new Error('Invalid login response.');
|
||||||
}
|
}
|
||||||
|
|
||||||
appDispatch({ type: 'LOGIN_SUCCESS', token: data.access_token });
|
appDispatch({
|
||||||
|
type: 'LOGIN_SUCCESS',
|
||||||
|
token: data.access_token,
|
||||||
|
tenant: data.tenant || null,
|
||||||
|
});
|
||||||
setStatusMessage('Login successful.', 'success');
|
setStatusMessage('Login successful.', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error?.response?.data?.error || 'Login failed. Check credentials.';
|
const message = error?.response?.data?.error || 'Login failed. Check credentials.';
|
||||||
@@ -5322,10 +5413,10 @@ const LoginRoute = () => {
|
|||||||
try {
|
try {
|
||||||
appDispatch({ type: 'LOGIN_REQUEST' });
|
appDispatch({ type: 'LOGIN_REQUEST' });
|
||||||
const { data } = await api.post('/auth/login', payload);
|
const { data } = await api.post('/auth/login', payload);
|
||||||
if (data?.selection_token && Array.isArray(data?.tenants)) {
|
if (data?.access_token && Array.isArray(data?.tenants)) {
|
||||||
appDispatch({
|
appDispatch({
|
||||||
type: 'TENANT_SELECTION_REQUIRED',
|
type: 'TENANT_SELECTION_REQUIRED',
|
||||||
selectionToken: data.selection_token,
|
selectionToken: data.access_token,
|
||||||
tenants: data.tenants,
|
tenants: data.tenants,
|
||||||
});
|
});
|
||||||
setStatusMessage('Select a tenant to continue.', 'info');
|
setStatusMessage('Select a tenant to continue.', 'info');
|
||||||
@@ -5336,7 +5427,11 @@ const LoginRoute = () => {
|
|||||||
throw new Error('Invalid login response.');
|
throw new Error('Invalid login response.');
|
||||||
}
|
}
|
||||||
|
|
||||||
appDispatch({ type: 'LOGIN_SUCCESS', token: data.access_token });
|
appDispatch({
|
||||||
|
type: 'LOGIN_SUCCESS',
|
||||||
|
token: data.access_token,
|
||||||
|
tenant: data.tenant || null,
|
||||||
|
});
|
||||||
setStatusMessage('Login successful.', 'success');
|
setStatusMessage('Login successful.', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error?.response?.data?.error || 'Login failed. Check credentials.';
|
const message = error?.response?.data?.error || 'Login failed. Check credentials.';
|
||||||
@@ -5369,7 +5464,11 @@ const LoginRoute = () => {
|
|||||||
throw new Error('Invalid tenant selection response.');
|
throw new Error('Invalid tenant selection response.');
|
||||||
}
|
}
|
||||||
|
|
||||||
appDispatch({ type: 'LOGIN_SUCCESS', token: data.access_token });
|
appDispatch({
|
||||||
|
type: 'LOGIN_SUCCESS',
|
||||||
|
token: data.access_token,
|
||||||
|
tenant: data.tenant || null,
|
||||||
|
});
|
||||||
setStatusMessage('Login successful.', 'success');
|
setStatusMessage('Login successful.', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error?.response?.data?.error || 'Failed to finalize login.';
|
const message = error?.response?.data?.error || 'Failed to finalize login.';
|
||||||
|
|||||||
Reference in New Issue
Block a user