feat(ui): add share modal and users

- fix(external users): fix app starting for external users
This commit is contained in:
Edouard Vanbelle
2026-06-02 12:25:44 +02:00
parent ec72374651
commit 9fac34f91e
15 changed files with 468 additions and 54 deletions
+28 -3
View File
@@ -3,7 +3,6 @@
*/
import { getCsrfHeaders } from '../core/csrf.js';
import { loadFiles } from './filesView.js';
import { updateStorageUsageDisplay } from './main.js';
import { app } from './state.js';
import { ui } from './ui.js';
@@ -40,6 +39,7 @@ async function refreshUserData() {
console.log('Storage from server: used=', userData.storage_used_bytes, 'quota=', userData.storage_quota_bytes);
localStorage.setItem(USER_DATA_KEY, JSON.stringify(userData));
app.isExternalUser = !!userData.is_external;
updateStorageUsageDisplay(userData);
return userData;
} catch (error) {
@@ -93,8 +93,15 @@ async function checkAuthentication() {
// Check session validity by calling /api/auth/me (cookie auto-sent)
console.log('Checking session via /api/auth/me...');
/** @type {User} */
/** @type {User} */
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
// Restore the external-user flag eagerly from cache so the
// resolveHomeFolder short-circuit fires before the /api/auth/me
// refresh completes. The parse may produce a sparse object on
// first load — `is_external` defaulting to falsy is correct
// for the internal-user-by-default contract.
app.isExternalUser = !!userData.is_external;
if (userData.username) {
// We have cached user data — render immediately, refresh in background
updateUserMenuData();
@@ -134,14 +141,22 @@ async function checkAuthentication() {
await resolveHomeFolder();
window.dispatchEvent(new CustomEvent('authenticationDone'));
} else {
// No cached user data — must verify session from server
// No cached user data — must verify session from server.
// This is the first-load path for magic-link redemptions
// (cookies set server-side, no prior localStorage).
console.log('No cached user data, fetching from server');
try {
const freshData = await refreshUserData();
if (freshData?.username) {
updateUserMenuData();
updateStorageUsageDisplay(freshData);
resolveHomeFolder().then(() => loadFiles());
await resolveHomeFolder();
// Defer to the `authenticationDone` listener in main.js
// so the hash-driven section + path init runs in one
// place (was previously a `loadFiles()` here which
// bypassed the hash context and produced
// `/api/folders//resources` for external users).
window.dispatchEvent(new CustomEvent('authenticationDone'));
} else {
console.warn('Could not retrieve user data, redirecting to login');
localStorage.removeItem(USER_DATA_KEY);
@@ -162,6 +177,16 @@ async function checkAuthentication() {
async function resolveHomeFolder() {
if (app.userHomeFolderId) return;
// External users (grant-only recipients) do not own a home folder
// by design — see `HomeFolderLifecycleHook::provision_if_needed`
// which short-circuits on `is_external`. Skip the fetch + leave
// `userHomeFolderId` null so downstream code knows to land them on
// /#/sharedwithme instead of /files.
if (app.isExternalUser) {
console.log('External user — skipping home-folder resolution');
app.breadcrumbPath = [];
return;
}
try {
const response = await fetch('/api/folders', {
credentials: 'same-origin'
+11
View File
@@ -405,6 +405,17 @@ async function loadFiles(options = { insertHistory: true }) {
try {
if (!app.userHomeFolderId) await resolveHomeFolder();
// External users have no home folder. If they land on /files
// without a specific folder id in the URL, redirect them to
// /#/sharedwithme — their actual landing page. This guards
// against `fetchResourcesPage('')` building `/api/folders//resources`.
if (app.isExternalUser && (!app.currentPath || app.currentPath === '')) {
clearTimeout(spinnerTimeout);
_loading = false;
window.location.hash = '#/sharedwithme';
return;
}
// Resolve path to home folder when none is set
if (!app.currentPath || app.currentPath === '') {
if (app.userHomeFolderId) {
+4 -1
View File
@@ -407,8 +407,11 @@ function setupActionsBarDelegation() {
function deserializeHash() {
const hashContext = /** type {OxiContext} */ {};
// External users have no home folder; default them to /#/sharedwithme
// (their actual landing) so the URL bar reflects what they'll see.
// Internal users default to the Files section.
// FIXME rename files into drive ?
hashContext.section = 'files';
hashContext.section = app.isExternalUser ? 'sharedwithme' : 'files';
const hash_elements = window.location.hash.split('/');
+9 -3
View File
@@ -299,9 +299,15 @@ function switchToFilesSection() {
//reset files view + remove any error
ui.resetFilesList();
// Reset to home folder and update breadcrumb
app.currentPath = app.userHomeFolderId || '';
app.breadcrumbPath = [];
// Reset to home folder and update breadcrumb. External users have no
// home — leave `currentPath` as the caller set it (e.g. the magic-link
// landing's hash context) so loadFiles() doesn't fall through to
// `/api/folders//resources`. If `currentPath` is still empty by the
// time loadFiles() runs, it self-redirects to /#/sharedwithme.
if (!app.isExternalUser) {
app.currentPath = app.userHomeFolderId || '';
app.breadcrumbPath = [];
}
ui.updateBreadcrumb();
if (batchToolbar) batchToolbar.clear();
+10
View File
@@ -59,6 +59,16 @@ export const app = {
/** @type {string | null} */
userHomeFolderName: null,
/**
* `true` when the authenticated caller is an external (grant-only)
* user. Externals don't own a home folder, can't enumerate users,
* and land on `/#/sharedwithme` by default. Set by `refreshUserData`
* and the cached-data load path from the `is_external` field of
* `/api/auth/me`'s response.
* @type {boolean}
*/
isExternalUser: false,
/** @type {Array<{id: string, name: string}>} */
breadcrumbPath: [], // Array of {id, name} tracking folder navigation hierarchy