mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
feat(sync): auto-detect Nextcloud user ID via OCS (#7617)
The Nextcloud WebDAV files path needs the account's internal user ID, which differs from the email/login name people enter and is awkward to find by hand — the root cause of the recurring '404 / connection test failed' reports. Add a 'Detect user ID' button to the Nextcloud sync config that asks the server for it. - packages/sync-providers: discoverNextcloudUserId() calls the OCS endpoint /ocs/v2.php/cloud/user (OCS-APIRequest header) authenticated with login + app password and returns ocs.data.id. A 401 reports bad credentials (cleanly distinct from the 404 wrong-user-id path); a 200 that isn't an OCS payload reports 'not a Nextcloud/OCS server'. A missing/wrong URL scheme is refused up front (matches the provider's own _cfgOrError check) so credentials never hit a schemeless host. - Dialog: button fills the Username field with the detected ID. To keep auth working, if 'Login name' was empty and the user had typed their login into 'Username', that login is preserved into 'Login name' before Username is overwritten with the ID. Result handling split into _applyDetectedUserIdResult for unit-testability. - Additive and optional: generic WebDAV and the save/sync path are untouched, no synced data shapes change. - Tests: 7 package specs (success, trailing-slash, login fallback, 401, non-OCS 200, missing id, scheme guard) + 6 dialog specs (login guard, fill+confirm, 3 login-preservation cases, failure).
This commit is contained in:
parent
b32f34bfb1
commit
4fb68c6885
8 changed files with 476 additions and 2 deletions
|
|
@ -0,0 +1,131 @@
|
|||
import type { SyncLogger } from '@sp/sync-core';
|
||||
import type { ProviderPlatformInfo } from '../../platform/provider-platform-info';
|
||||
import type { WebFetchFactory } from '../../platform/web-fetch-factory';
|
||||
import type { NativeHttpExecutor } from '../../http/native-http-retry';
|
||||
import { AuthFailSPError } from '../../errors';
|
||||
import { errorMeta } from '../../log/error-meta';
|
||||
import { WebDavHttpAdapter } from './webdav-http-adapter';
|
||||
import { WebDavHttpHeader, WebDavHttpMethod, WebDavHttpStatus } from './webdav.const';
|
||||
import { NextcloudProvider } from './nextcloud';
|
||||
import type { NextcloudPrivateCfg } from './nextcloud.model';
|
||||
|
||||
/** OCS APIs reject requests that lack this header (CSRF guard). */
|
||||
const OCS_API_REQUEST_HEADER = 'OCS-APIRequest';
|
||||
|
||||
export interface DiscoverNextcloudUserIdDeps {
|
||||
logger: SyncLogger;
|
||||
platformInfo: ProviderPlatformInfo;
|
||||
webFetch: WebFetchFactory;
|
||||
nativeHttp: NativeHttpExecutor;
|
||||
}
|
||||
|
||||
export interface DiscoverNextcloudUserIdResult {
|
||||
success: boolean;
|
||||
userId?: string;
|
||||
error?: string;
|
||||
errorCode?: number;
|
||||
}
|
||||
|
||||
type DiscoverCfg = Pick<
|
||||
NextcloudPrivateCfg,
|
||||
'serverUrl' | 'userName' | 'loginName' | 'password'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Ask a Nextcloud server for the authenticated account's canonical user ID
|
||||
* via the OCS API, so the user never has to look it up by hand (issue #7617:
|
||||
* the WebDAV files path needs the internal user ID, which differs from the
|
||||
* email/login name people naturally enter and is awkward to find manually).
|
||||
*
|
||||
* Authenticates with `loginName || userName` + app password — whichever the
|
||||
* user filled in — and returns `<ocs.data.id>`. A 401 means the credentials
|
||||
* are wrong (cleanly distinct from the 404 "wrong user ID" path); a 200 that
|
||||
* isn't an OCS payload means the server isn't Nextcloud / has OCS disabled.
|
||||
*
|
||||
* Same readability/privacy contract as `testConnection`: the returned `error`
|
||||
* is human-readable for the config UI and must never be routed to a structured
|
||||
* logger (the privacy invariant lives in the `errorMeta`-routed log below).
|
||||
*/
|
||||
export const discoverNextcloudUserId = async (
|
||||
cfg: DiscoverCfg,
|
||||
deps: DiscoverNextcloudUserIdDeps,
|
||||
): Promise<DiscoverNextcloudUserIdResult> => {
|
||||
// Match the provider's own `_cfgOrError` scheme check: refuse a missing/wrong
|
||||
// scheme up front so credentials are never sent to a schemeless/typo'd host,
|
||||
// and the user gets a clear message instead of a raw fetch TypeError.
|
||||
if (!/^https?:\/\//i.test(cfg.serverUrl?.trim() ?? '')) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Server URL must start with https:// or http://',
|
||||
};
|
||||
}
|
||||
|
||||
const httpAdapter = new WebDavHttpAdapter({
|
||||
logger: deps.logger,
|
||||
platformInfo: deps.platformInfo,
|
||||
webFetch: deps.webFetch,
|
||||
nativeHttp: deps.nativeHttp,
|
||||
});
|
||||
|
||||
try {
|
||||
const auth = btoa(`${NextcloudProvider.getAuthUserName(cfg)}:${cfg.password}`);
|
||||
const response = await httpAdapter.request({
|
||||
url: _buildOcsUserUrl(cfg.serverUrl),
|
||||
method: WebDavHttpMethod.GET,
|
||||
headers: {
|
||||
[WebDavHttpHeader.AUTHORIZATION]: `Basic ${auth}`,
|
||||
[OCS_API_REQUEST_HEADER]: 'true',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const userId = _parseOcsUserId(response.data);
|
||||
if (userId) {
|
||||
return { success: true, userId };
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'The server responded but did not return a user ID. ' +
|
||||
'Is this a Nextcloud server with the OCS API enabled?',
|
||||
};
|
||||
} catch (e) {
|
||||
deps.logger.normal('discoverNextcloudUserId() failed', errorMeta(e));
|
||||
if (e instanceof AuthFailSPError) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'Authentication failed (HTTP 401). Check your login name / email and app password.',
|
||||
errorCode: WebDavHttpStatus.UNAUTHORIZED,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: e instanceof Error ? e.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/** `https://host[/base]` (+ optional trailing slash) -> OCS current-user URL. */
|
||||
const _buildOcsUserUrl = (serverUrl: string): string => {
|
||||
let s = serverUrl.trim();
|
||||
if (s.endsWith('/')) {
|
||||
s = s.slice(0, -1);
|
||||
}
|
||||
return `${s}/ocs/v2.php/cloud/user?format=json`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pull `ocs.data.id` out of an OCS JSON response. Returns null on anything
|
||||
* unexpected (non-JSON login-page HTML, missing/empty id) so the caller can
|
||||
* surface a "not a Nextcloud OCS endpoint" message rather than throwing.
|
||||
*/
|
||||
const _parseOcsUserId = (body: string): string | null => {
|
||||
try {
|
||||
const parsed = JSON.parse(body) as { ocs?: { data?: { id?: unknown } } };
|
||||
const id = parsed?.ocs?.data?.id;
|
||||
return typeof id === 'string' && id.trim() ? id.trim() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
|
@ -13,3 +13,8 @@ export {
|
|||
testWebdavConnection,
|
||||
type TestWebdavConnectionDeps,
|
||||
} from './file-based/webdav/test-connection';
|
||||
export {
|
||||
discoverNextcloudUserId,
|
||||
type DiscoverNextcloudUserIdDeps,
|
||||
type DiscoverNextcloudUserIdResult,
|
||||
} from './file-based/webdav/discover-nextcloud-user-id';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { NOOP_SYNC_LOGGER, type SyncLogger } from '@sp/sync-core';
|
||||
import { discoverNextcloudUserId } from '../../../src/webdav';
|
||||
import type { NativeHttpExecutor } from '../../../src/http';
|
||||
import type { ProviderPlatformInfo, WebFetchFactory } from '../../../src/platform';
|
||||
|
||||
const ocsBody = (id: unknown): string =>
|
||||
JSON.stringify({ ocs: { meta: { status: 'ok', statuscode: 200 }, data: { id } } });
|
||||
|
||||
interface RecordedReq {
|
||||
url: string;
|
||||
init?: RequestInit;
|
||||
}
|
||||
|
||||
const makeDeps = (
|
||||
fetchImpl: typeof fetch,
|
||||
): {
|
||||
logger: SyncLogger;
|
||||
platformInfo: ProviderPlatformInfo;
|
||||
webFetch: WebFetchFactory;
|
||||
nativeHttp: NativeHttpExecutor;
|
||||
} => ({
|
||||
logger: NOOP_SYNC_LOGGER,
|
||||
platformInfo: {
|
||||
isNativePlatform: false,
|
||||
isAndroidWebView: false,
|
||||
isIosNative: false,
|
||||
},
|
||||
webFetch: () => fetchImpl,
|
||||
nativeHttp: vi.fn(),
|
||||
});
|
||||
|
||||
const recordingFetch = (calls: RecordedReq[], response: () => Response): typeof fetch =>
|
||||
vi.fn(async (url: string, init?: RequestInit) => {
|
||||
calls.push({ url, init });
|
||||
return response();
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
const decodeBasic = (headers: Record<string, string> | undefined): string =>
|
||||
atob((headers?.['Authorization'] ?? '').replace('Basic ', ''));
|
||||
|
||||
const CFG = {
|
||||
serverUrl: 'https://cloud.example.com',
|
||||
userName: '',
|
||||
loginName: 'jane@example.com',
|
||||
password: 'app-pw',
|
||||
};
|
||||
|
||||
describe('discoverNextcloudUserId', () => {
|
||||
it('returns the OCS user id and queries the OCS endpoint with the OCS header + login auth', async () => {
|
||||
const calls: RecordedReq[] = [];
|
||||
const res = await discoverNextcloudUserId(
|
||||
CFG,
|
||||
makeDeps(
|
||||
recordingFetch(calls, () => new Response(ocsBody('janedoe'), { status: 200 })),
|
||||
),
|
||||
);
|
||||
|
||||
expect(res).toEqual({ success: true, userId: 'janedoe' });
|
||||
expect(calls[0]?.url).toBe(
|
||||
'https://cloud.example.com/ocs/v2.php/cloud/user?format=json',
|
||||
);
|
||||
const headers = calls[0]?.init?.headers as Record<string, string>;
|
||||
expect(headers['OCS-APIRequest']).toBe('true');
|
||||
expect(decodeBasic(headers)).toBe('jane@example.com:app-pw');
|
||||
});
|
||||
|
||||
it('trims a trailing slash from the server URL', async () => {
|
||||
const calls: RecordedReq[] = [];
|
||||
await discoverNextcloudUserId(
|
||||
{ ...CFG, serverUrl: 'https://cloud.example.com/' },
|
||||
makeDeps(recordingFetch(calls, () => new Response(ocsBody('x'), { status: 200 }))),
|
||||
);
|
||||
expect(calls[0]?.url).toBe(
|
||||
'https://cloud.example.com/ocs/v2.php/cloud/user?format=json',
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to userName for auth when loginName is empty', async () => {
|
||||
const calls: RecordedReq[] = [];
|
||||
await discoverNextcloudUserId(
|
||||
{ ...CFG, loginName: '', userName: 'plainuser' },
|
||||
makeDeps(
|
||||
recordingFetch(calls, () => new Response(ocsBody('uid'), { status: 200 })),
|
||||
),
|
||||
);
|
||||
expect(decodeBasic(calls[0]?.init?.headers as Record<string, string>)).toBe(
|
||||
'plainuser:app-pw',
|
||||
);
|
||||
});
|
||||
|
||||
it('reports a 401 as wrong credentials (distinct from the 404 wrong-user-id path)', async () => {
|
||||
const res = await discoverNextcloudUserId(
|
||||
CFG,
|
||||
makeDeps(recordingFetch([], () => new Response('', { status: 401 }))),
|
||||
);
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.errorCode).toBe(401);
|
||||
expect(res.error).toContain('Authentication failed');
|
||||
});
|
||||
|
||||
it('reports a non-OCS 200 response without throwing (server is not Nextcloud / OCS disabled)', async () => {
|
||||
const res = await discoverNextcloudUserId(
|
||||
CFG,
|
||||
makeDeps(
|
||||
recordingFetch(
|
||||
[],
|
||||
() => new Response('<!DOCTYPE html><html>login</html>', { status: 200 }),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.userId).toBeUndefined();
|
||||
expect(res.error).toContain('did not return a user ID');
|
||||
});
|
||||
|
||||
it('rejects a server URL without an http(s) scheme without sending credentials', async () => {
|
||||
const calls: RecordedReq[] = [];
|
||||
const res = await discoverNextcloudUserId(
|
||||
{ ...CFG, serverUrl: 'cloud.example.com' },
|
||||
makeDeps(recordingFetch(calls, () => new Response(ocsBody('x'), { status: 200 }))),
|
||||
);
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.error).toContain('https://');
|
||||
expect(calls.length).toBe(0);
|
||||
});
|
||||
|
||||
it('treats a missing/non-string id in the OCS payload as a failure', async () => {
|
||||
const res = await discoverNextcloudUserId(
|
||||
CFG,
|
||||
makeDeps(recordingFetch([], () => new Response(ocsBody(12345), { status: 200 }))),
|
||||
);
|
||||
expect(res.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { provideNoopAnimations } from '@angular/platform-browser/animations';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
|
@ -268,6 +269,99 @@ describe('DialogSyncCfgComponent', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Nextcloud detect user ID (#7617)', () => {
|
||||
it('asks for the login/password before calling the server', async () => {
|
||||
await (component as any)._detectNextcloudUserId({
|
||||
serverUrl: '',
|
||||
loginName: '',
|
||||
userName: '',
|
||||
password: '',
|
||||
});
|
||||
expect(mockSnackService.open).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
msg: T.F.SYNC.FORM.NEXTCLOUD.S_DETECT_USER_ID_NEED_LOGIN,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const buildNextcloudForm = (userName: string, loginName: string): void => {
|
||||
component.form = new FormGroup({
|
||||
nextcloud: new FormGroup({
|
||||
userName: new FormControl(userName),
|
||||
loginName: new FormControl(loginName),
|
||||
}),
|
||||
}) as any;
|
||||
};
|
||||
const valueOf = (key: string): unknown =>
|
||||
(component.form.get(`nextcloud.${key}`) as FormControl | null)?.value;
|
||||
|
||||
it('fills the Username field with the detected user ID and confirms', () => {
|
||||
buildNextcloudForm('janedoe', 'jane@example.com');
|
||||
|
||||
(component as any)._applyDetectedUserIdResult({ success: true, userId: 'janedoe' });
|
||||
|
||||
expect(valueOf('userName')).toBe('janedoe');
|
||||
expect(mockSnackService.open).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
type: 'SUCCESS',
|
||||
msg: T.F.SYNC.FORM.NEXTCLOUD.S_DETECT_USER_ID_SUCCESS,
|
||||
translateParams: { userId: 'janedoe' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves the typed login: moves it to Login name when Login name was empty', () => {
|
||||
// User put their email in "Username" (which authenticated) and left
|
||||
// "Login name" empty — keep the email as login so auth still works.
|
||||
buildNextcloudForm('jane@example.com', '');
|
||||
|
||||
(component as any)._applyDetectedUserIdResult({ success: true, userId: 'janedoe' });
|
||||
|
||||
expect(valueOf('userName')).toBe('janedoe');
|
||||
expect(valueOf('loginName')).toBe('jane@example.com');
|
||||
});
|
||||
|
||||
it('does not overwrite an existing Login name', () => {
|
||||
buildNextcloudForm('', 'jane@example.com');
|
||||
|
||||
(component as any)._applyDetectedUserIdResult({ success: true, userId: 'janedoe' });
|
||||
|
||||
expect(valueOf('userName')).toBe('janedoe');
|
||||
expect(valueOf('loginName')).toBe('jane@example.com');
|
||||
});
|
||||
|
||||
it('leaves Login name empty when Username already equals the detected ID', () => {
|
||||
buildNextcloudForm('janedoe', '');
|
||||
|
||||
(component as any)._applyDetectedUserIdResult({ success: true, userId: 'janedoe' });
|
||||
|
||||
expect(valueOf('userName')).toBe('janedoe');
|
||||
expect(valueOf('loginName')).toBe('');
|
||||
});
|
||||
|
||||
it('surfaces the failure message (e.g. a 401) without touching the form', () => {
|
||||
component.form = new FormGroup({
|
||||
nextcloud: new FormGroup({ userName: new FormControl('keep-me') }),
|
||||
}) as any;
|
||||
|
||||
(component as any)._applyDetectedUserIdResult({
|
||||
success: false,
|
||||
error: 'Authentication failed (HTTP 401).',
|
||||
});
|
||||
|
||||
expect(
|
||||
(component.form.get('nextcloud.userName') as FormControl | null)?.value,
|
||||
).toBe('keep-me');
|
||||
expect(mockSnackService.open).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
type: 'ERROR',
|
||||
msg: T.F.SYNC.FORM.NEXTCLOUD.S_DETECT_USER_ID_FAIL,
|
||||
translateParams: { error: 'Authentication failed (HTTP 401).' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('save() — already configured provider', () => {
|
||||
it('should close dialog when Dropbox is already configured (wasConfigured=false, isReady=true)', async () => {
|
||||
const alreadyConfigured = {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import {
|
|||
SYNC_FORM,
|
||||
SyncCollapsibleProps,
|
||||
} from '../../../features/config/form-cfgs/sync-form.const';
|
||||
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { AbstractControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
|
||||
import { SyncConfig } from '../../../features/config/global-config.model';
|
||||
import {
|
||||
|
|
@ -47,6 +47,7 @@ import {
|
|||
type WebdavPrivateCfg,
|
||||
} from '@sp/sync-providers/webdav';
|
||||
import { testWebdavConnection } from '../../../op-log/sync-providers/file-based/webdav/test-webdav-connection';
|
||||
import { discoverNextcloudUserId } from '../../../op-log/sync-providers/file-based/webdav/discover-nextcloud-user-id';
|
||||
import type { OneDrivePrivateCfg } from '../../../op-log/sync-providers/file-based/onedrive/onedrive.model';
|
||||
|
||||
// `testWebdavConnection` reports a 404 (auth ok, wrong DAV path) via this
|
||||
|
|
@ -113,7 +114,11 @@ export class DialogSyncCfgComponent implements AfterViewInit {
|
|||
if (item.key === 'nextcloud' && item.fieldGroup) {
|
||||
return {
|
||||
...item,
|
||||
fieldGroup: [...item.fieldGroup, this._nextcloudTestConnectionBtn()],
|
||||
fieldGroup: [
|
||||
...item.fieldGroup,
|
||||
this._nextcloudDetectUserIdBtn(),
|
||||
this._nextcloudTestConnectionBtn(),
|
||||
],
|
||||
};
|
||||
}
|
||||
if (
|
||||
|
|
@ -194,6 +199,14 @@ export class DialogSyncCfgComponent implements AfterViewInit {
|
|||
});
|
||||
}
|
||||
|
||||
private _nextcloudDetectUserIdBtn(): FormlyFieldConfig {
|
||||
return this._actionBtn({
|
||||
text: T.F.SYNC.FORM.NEXTCLOUD.L_DETECT_USER_ID,
|
||||
className: 'mt3 block',
|
||||
onClick: (model) => this._detectNextcloudUserId(model as NextcloudPrivateCfg),
|
||||
});
|
||||
}
|
||||
|
||||
private _forceOverwriteBtn(): FormlyFieldConfig {
|
||||
return this._actionBtn({
|
||||
text: T.F.SYNC.S.BTN_FORCE_OVERWRITE,
|
||||
|
|
@ -252,6 +265,69 @@ export class DialogSyncCfgComponent implements AfterViewInit {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the Nextcloud server for the account's canonical user ID and write it
|
||||
* into the "Username" field, so users don't have to hunt for it by hand
|
||||
* (issue #7617). Authenticates with the login name / email (or whatever is
|
||||
* in "Username") + app password; a 401 cleanly reports bad credentials.
|
||||
*/
|
||||
private async _detectNextcloudUserId(cfg: NextcloudPrivateCfg): Promise<void> {
|
||||
const login = cfg?.loginName?.trim() || cfg?.userName?.trim();
|
||||
if (!cfg?.serverUrl?.trim() || !login || !cfg?.password) {
|
||||
this._snackService.open({
|
||||
type: 'ERROR',
|
||||
msg: T.F.SYNC.FORM.NEXTCLOUD.S_DETECT_USER_ID_NEED_LOGIN,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._applyDetectedUserIdResult(await discoverNextcloudUserId(cfg));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a `discoverNextcloudUserId` result: on success fill the "Username"
|
||||
* field with the detected user ID and confirm; otherwise surface the
|
||||
* readable failure. Split from the network call so it is unit-testable
|
||||
* without a live server (mirrors `_reportWebdavTestResult`).
|
||||
*/
|
||||
private _applyDetectedUserIdResult(result: {
|
||||
success: boolean;
|
||||
userId?: string;
|
||||
error?: string;
|
||||
}): void {
|
||||
if (result.success && result.userId) {
|
||||
// formly builds the form dynamically, so the controls are untyped here.
|
||||
const userNameCtrl = this.form.get('nextcloud.userName') as AbstractControl | null;
|
||||
const loginNameCtrl = this.form.get(
|
||||
'nextcloud.loginName',
|
||||
) as AbstractControl | null;
|
||||
// Preserve the credential that just authenticated: if "Login name" is
|
||||
// empty and the user typed their login (e.g. email) into "Username",
|
||||
// keep it as the login name before overwriting Username with the user
|
||||
// ID — otherwise the next sync would auth as the ID, which some servers
|
||||
// reject (turning a wrong-path 404 into an auth 401). See issue #7617.
|
||||
const priorUserName = ((userNameCtrl?.value as string) ?? '').trim();
|
||||
if (
|
||||
!((loginNameCtrl?.value as string) ?? '').trim() &&
|
||||
priorUserName &&
|
||||
priorUserName !== result.userId
|
||||
) {
|
||||
loginNameCtrl?.setValue(priorUserName);
|
||||
}
|
||||
userNameCtrl?.setValue(result.userId);
|
||||
this._snackService.open({
|
||||
type: 'SUCCESS',
|
||||
msg: T.F.SYNC.FORM.NEXTCLOUD.S_DETECT_USER_ID_SUCCESS,
|
||||
translateParams: { userId: result.userId },
|
||||
});
|
||||
} else {
|
||||
this._snackService.open({
|
||||
type: 'ERROR',
|
||||
msg: T.F.SYNC.FORM.NEXTCLOUD.S_DETECT_USER_ID_FAIL,
|
||||
translateParams: { error: result.error || 'Unknown error' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _testWebDavConnection(
|
||||
webDavCfg: WebdavPrivateCfg,
|
||||
notFoundMsg?: string,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import {
|
||||
discoverNextcloudUserId as packageDiscoverNextcloudUserId,
|
||||
type DiscoverNextcloudUserIdResult,
|
||||
type NextcloudPrivateCfg,
|
||||
} from '@sp/sync-providers/webdav';
|
||||
import { OP_LOG_SYNC_LOGGER } from '../../../core/sync-logger.adapter';
|
||||
import { APP_PROVIDER_PLATFORM_INFO } from '../../platform/app-provider-platform-info';
|
||||
import { APP_WEB_FETCH } from '../../platform/app-web-fetch';
|
||||
import { APP_WEBDAV_NATIVE_HTTP } from './capacitor-webdav-http/app-webdav-native-http';
|
||||
|
||||
/**
|
||||
* App-side wrapper around the package's `discoverNextcloudUserId` helper.
|
||||
* Composes the four app singletons (logger, platform info, web fetch,
|
||||
* native HTTP) so the sync dialog only needs the draft cfg.
|
||||
*/
|
||||
export const discoverNextcloudUserId = (
|
||||
cfg: Pick<NextcloudPrivateCfg, 'serverUrl' | 'userName' | 'loginName' | 'password'>,
|
||||
): Promise<DiscoverNextcloudUserIdResult> =>
|
||||
packageDiscoverNextcloudUserId(cfg, {
|
||||
logger: OP_LOG_SYNC_LOGGER,
|
||||
platformInfo: APP_PROVIDER_PLATFORM_INFO,
|
||||
webFetch: APP_WEB_FETCH,
|
||||
nativeHttp: APP_WEBDAV_NATIVE_HTTP,
|
||||
});
|
||||
|
|
@ -1497,6 +1497,11 @@ const T = {
|
|||
SERVER_URL_DESCRIPTION: 'F.SYNC.FORM.NEXTCLOUD.SERVER_URL_DESCRIPTION',
|
||||
L_APP_PASSWORD: 'F.SYNC.FORM.NEXTCLOUD.L_APP_PASSWORD',
|
||||
APP_PASSWORD_DESCRIPTION: 'F.SYNC.FORM.NEXTCLOUD.APP_PASSWORD_DESCRIPTION',
|
||||
L_DETECT_USER_ID: 'F.SYNC.FORM.NEXTCLOUD.L_DETECT_USER_ID',
|
||||
S_DETECT_USER_ID_NEED_LOGIN:
|
||||
'F.SYNC.FORM.NEXTCLOUD.S_DETECT_USER_ID_NEED_LOGIN',
|
||||
S_DETECT_USER_ID_SUCCESS: 'F.SYNC.FORM.NEXTCLOUD.S_DETECT_USER_ID_SUCCESS',
|
||||
S_DETECT_USER_ID_FAIL: 'F.SYNC.FORM.NEXTCLOUD.S_DETECT_USER_ID_FAIL',
|
||||
S_TEST_FAIL_USER_NOT_FOUND: 'F.SYNC.FORM.NEXTCLOUD.S_TEST_FAIL_USER_NOT_FOUND',
|
||||
},
|
||||
ONEDRIVE: {
|
||||
|
|
|
|||
|
|
@ -1453,6 +1453,10 @@
|
|||
"SERVER_URL_DESCRIPTION": "e.g. https://cloud.example.com or https://example.com/nextcloud",
|
||||
"L_APP_PASSWORD": "App Password",
|
||||
"APP_PASSWORD_DESCRIPTION": "Go to Nextcloud → Settings → Security → App passwords to generate one",
|
||||
"L_DETECT_USER_ID": "Detect user ID",
|
||||
"S_DETECT_USER_ID_NEED_LOGIN": "Enter your Server URL, your login (Username or Login name / email), and your App Password first.",
|
||||
"S_DETECT_USER_ID_SUCCESS": "Found your Nextcloud user ID: {{userId}}. It has been filled into the Username field.",
|
||||
"S_DETECT_USER_ID_FAIL": "Could not detect your user ID: {{error}}",
|
||||
"S_TEST_FAIL_USER_NOT_FOUND": "Connection test failed: user folder not found (404). Your login works, but \"Username\" must be your Nextcloud user ID — not your email or display name. To find it, open Files in Nextcloud, click the settings gear (bottom-left corner), and copy the <user-id> from the WebDAV URL shown there (.../remote.php/dav/files/<user-id>/). Target URL: {{url}}"
|
||||
},
|
||||
"ONEDRIVE": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue