mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-16 12:47:19 +08:00
feat(seafile): add library and directory sync scope support (#13153)
### What problem does this PR solve?
The SeaFile connector currently synchronises the entire account — every
library
visible to the authenticated user. This is impractical for users who
only need
a subset of their data indexed, especially on large SeaFile instances
with many
shared libraries.
This PR introduces granular sync scope support, allowing users to choose
between
syncing their entire account, a single library, or a specific directory
within a
library. It also adds support for SeaFile library-scoped API tokens
(`/api/v2.1/via-repo-token/` endpoints), enabling tighter access control
without
exposing account-level credentials.
### Type of change
- [ ] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [ ] Documentation Update
- [ ] Refactoring
- [ ] Performance Improvement
- [ ] Other (please describe):
### Test
```
from seafile_connector import SeaFileConnector
import logging
import os
logging.basicConfig(level=logging.DEBUG)
URL = os.environ.get("SEAFILE_URL", "https://seafile.example.com")
TOKEN = os.environ.get("SEAFILE_TOKEN", "")
REPO_ID = os.environ.get("SEAFILE_REPO_ID", "")
SYNC_PATH = os.environ.get("SEAFILE_SYNC_PATH", "/Documents")
REPO_TOKEN = os.environ.get("SEAFILE_REPO_TOKEN", "")
def _test_scope(scope, repo_id=None, sync_path=None):
print(f"\n{'='*50}")
print(f"Testing scope: {scope}")
print(f"{'='*50}")
creds = {"seafile_token": TOKEN} if TOKEN else {}
if REPO_TOKEN and scope in ("library", "directory"):
creds["repo_token"] = REPO_TOKEN
connector = SeaFileConnector(
seafile_url=URL,
batch_size=5,
sync_scope=scope,
include_shared = False,
repo_id=repo_id,
sync_path=sync_path,
)
connector.load_credentials(creds)
connector.validate_connector_settings()
count = 0
for batch in connector.load_from_state():
for doc in batch:
count += 1
print(f" [{count}] {doc.semantic_identifier} "
f"({doc.size_bytes} bytes, {doc.extension})")
print(f"\n-> {scope} scope: {count} document(s) found.\n")
# 1. Account scope
if TOKEN:
_test_scope("account")
else:
print("\nSkipping account scope (set SEAFILE_TOKEN)")
# 2. Library scope
if REPO_ID and (TOKEN or REPO_TOKEN):
_test_scope("library", repo_id=REPO_ID)
else:
print("\nSkipping library scope (set SEAFILE_REPO_ID + token)")
# 3. Directory scope
if REPO_ID and SYNC_PATH and (TOKEN or REPO_TOKEN):
_test_scope("directory", repo_id=REPO_ID, sync_path=SYNC_PATH)
else:
print("\nSkipping directory scope (set SEAFILE_REPO_ID + SEAFILE_SYNC_PATH + token)")
```
This commit is contained in:
committed by
GitHub
parent
aec2ef4232
commit
d1afcc9e71
@@ -1031,13 +1031,57 @@ Beispiel: Virtual Hosted Style`,
|
||||
seafileDescription:
|
||||
'Verbinden Sie sich mit Ihrem SeaFile-Server, um Dateien und Dokumente aus Ihren Bibliotheken zu synchronisieren.',
|
||||
seafileUrlTip:
|
||||
'Die Basis-URL Ihres SeaFile-Servers (z.B. https://seafile.example.com). Fügen Sie kein /api2 oder andere Pfade hinzu.',
|
||||
'Die vollstaendige URL Ihres SeaFile-Servers inklusive Protokoll. Beispiel: https://seafile.example.com - Kein abschliessender Schraegstrich und kein Pfad nach der Domain.',
|
||||
seafileAccountScopeTip:
|
||||
'Synchronisiert alle Bibliotheken, die für den unten angegebenen Konto-API-Token sichtbar sind.',
|
||||
seafileTokenPanelHeading:
|
||||
'Wählen Sie eine der folgenden Authentifizierungsmethoden:',
|
||||
seafileTokenPanelAccountBullet:
|
||||
'- gewährt Zugriff auf alle Ihre Bibliotheken.',
|
||||
seafileTokenPanelLibraryBullet:
|
||||
'- auf eine einzelne Bibliothek beschränkt (sicherer).',
|
||||
seafileValidationAccountTokenRequired:
|
||||
'Konto-API-Token ist erforderlich für den Umfang „Gesamtes Konto"',
|
||||
seafileValidationTokenRequired:
|
||||
'Geben Sie entweder einen Konto-API-Token oder einen Bibliotheks-Token an',
|
||||
seafileValidationLibraryIdRequired:
|
||||
'Bibliotheks-ID ist erforderlich',
|
||||
seafileValidationDirectoryPathRequired:
|
||||
'Verzeichnispfad ist erforderlich',
|
||||
seafileSyncScopeTip:
|
||||
'Legt fest, was synchronisiert wird: ' +
|
||||
'(1) Gesamtes Konto - Synchronisiert alle Bibliotheken, auf die Ihr Token Zugriff hat. Erfordert einen Konto-API-Token. ' +
|
||||
'(2) Einzelne Bibliothek - Synchronisiert alle Dateien innerhalb einer bestimmten Bibliothek. Erfordert die Bibliotheks-ID und entweder einen Konto-API-Token oder einen Bibliotheks-API-Token. ' +
|
||||
'(3) Bestimmtes Verzeichnis - Synchronisiert nur Dateien in einem bestimmten Ordner innerhalb einer Bibliothek. Erfordert die Bibliotheks-ID, den Ordnerpfad innerhalb dieser Bibliothek und entweder einen Konto-API-Token oder einen Bibliotheks-API-Token.',
|
||||
seafileTokenTip:
|
||||
'Erstellen Sie ein API-Token in SeaFile: Gehen Sie zu Einstellungen → API-Token → Token erstellen. Das Token ermöglicht den Zugriff auf alle für Ihr Konto sichtbaren Bibliotheken.',
|
||||
'Ihr kontoweiter SeaFile-API-Token. ' +
|
||||
'Gewährt Zugriff auf alle fuer Ihr Konto sichtbaren Bibliotheken. ' +
|
||||
'Erforderlich bei Synchronisierungsumfang "Gesamtes Konto". ' +
|
||||
'Für "Einzelne Bibliothek" oder "Bestimmtes Verzeichnis" können Sie alternativ einen Bibliotheks-API-Token verwenden.',
|
||||
seafileRepoTokenTip:
|
||||
'Ein bibliotheksbezogener API-Token, der nur Zugriff auf eine bestimmte Bibliothek gewährt. ' +
|
||||
'Kann anstelle des Konto-API-Tokens für "Einzelne Bibliothek" und "Bestimmtes Verzeichnis" verwendet werden.',
|
||||
seafileRepoIdTip:
|
||||
'Die eindeutige Kennung (UUID) der SeaFile-Bibliothek. ' +
|
||||
'Sie finden diese in der Adressleiste Ihres Browsers, wenn Sie die Bibliothek in der SeaFile-Weboberflaeche öffnen. ' +
|
||||
'Beispiel: 7a9e1b3c-4d5f-6a7b-8c9d-0e1f2a3b4c5d. ' +
|
||||
'Erforderlich bei Synchronisierungsumfang "Einzelne Bibliothek" oder "Bestimmtes Verzeichnis".',
|
||||
seafileSyncPathTip:
|
||||
'Der absolute Pfad des zu synchronisierenden Ordners innerhalb der oben angegebenen Bibliothek. ' +
|
||||
'Muss mit einem Schraegstrich beginnen. ' +
|
||||
'Alle Dateien und Unterordner unter diesem Pfad werden rekursiv einbezogen. ' +
|
||||
'Beispiel: /Dokumente/Berichte. ' +
|
||||
'Wichtig: Der Ordner muss innerhalb der angegebenen Bibliothek existieren. ' +
|
||||
'Pfade ausserhalb der Bibliothek werden nicht unterstuetzt. ' +
|
||||
'Wird nur verwendet bei Synchronisierungsumfang "Bestimmtes Verzeichnis".',
|
||||
seafileIncludeSharedTip:
|
||||
'Wenn aktiviert, werden auch Bibliotheken synchronisiert, die andere Benutzer mit Ihnen geteilt haben.',
|
||||
'Wenn aktiviert, werden auch Bibliotheken synchronisiert, die andere Benutzer mit Ihnen geteilt haben. ' +
|
||||
'Wenn deaktiviert, werden nur Bibliotheken synchronisiert, die Ihrem Konto gehoeren. ' +
|
||||
'Gilt nur bei Synchronisierungsumfang "Gesamtes Konto".',
|
||||
seafileBatchSizeTip:
|
||||
'Anzahl der Dokumente, die pro Batch verarbeitet werden. Höhere Werte können die Leistung verbessern, benötigen aber mehr Arbeitsspeicher. Standard: 100.',
|
||||
'Anzahl der Dokumente, die pro Durchlauf verarbeitet und zurueckgegeben werden. ' +
|
||||
'Ein kleinerer Wert verbraucht weniger Arbeitsspeicher, kann aber insgesamt langsamer sein. ' +
|
||||
'Standardwert: 100.',
|
||||
jiraDescription:
|
||||
'Verbinden Sie Ihren Jira-Arbeitsbereich, um Vorgänge, Kommentare und Anhänge zu synchronisieren.',
|
||||
jiraBaseUrlTip:
|
||||
|
||||
@@ -1007,15 +1007,59 @@ Example: Virtual Hosted Style`,
|
||||
moodleTokenTip:
|
||||
'Generate a web service token in Moodle: Go to Site administration → Server → Web services → Manage tokens. The user must be enrolled in the courses you want to sync.',
|
||||
seafileDescription:
|
||||
'Connect to your SeaFile server to sync files and documents from your libraries.',
|
||||
'Connect to your SeaFile server to synchronise files and documents from your libraries.',
|
||||
seafileUrlTip:
|
||||
'The base URL of your SeaFile server (e.g., https://seafile.example.com). Do not include /api2 or other paths.',
|
||||
'The full URL of your SeaFile server including the protocol. Example: https://seafile.example.com - Do not include a trailing slash or any path after the domain.',
|
||||
seafileAccountScopeTip:
|
||||
'Syncs all libraries visible to the Account API Token below.',
|
||||
seafileTokenPanelHeading:
|
||||
'Provide one of these authentication methods:',
|
||||
seafileTokenPanelAccountBullet:
|
||||
'- grants access to all your libraries.',
|
||||
seafileTokenPanelLibraryBullet:
|
||||
'— scoped to a single library only (more secure).',
|
||||
seafileValidationAccountTokenRequired:
|
||||
'Account API Token is required for Entire Account scope',
|
||||
seafileValidationTokenRequired:
|
||||
'Provide either an Account API Token or a Library Token',
|
||||
seafileValidationLibraryIdRequired:
|
||||
'Library ID is required',
|
||||
seafileValidationDirectoryPathRequired:
|
||||
'Directory Path is required',
|
||||
seafileSyncScopeTip:
|
||||
'Controls what gets synchronised: ' +
|
||||
'(1) Entire Account - Syncs all libraries your token has access to. Requires an Account API Token. ' +
|
||||
'(2) Single Library - Syncs all files within one specific library. Requires the Library ID and either an Account API Token or a Library API Token. ' +
|
||||
'(3) Specific Directory - Syncs only files within a specific folder inside a library. Requires the Library ID, the folder path within that library, and either an Account API Token or a Library API Token.',
|
||||
seafileTokenTip:
|
||||
'Generate an API token in SeaFile: Go to Settings → API Token → Generate Token. The token provides access to all libraries visible to your account.',
|
||||
'Your account-level SeaFile API token. ' +
|
||||
'Grants access to all libraries visible to your account. ' +
|
||||
'Required when sync scope is "Entire Account". ' +
|
||||
'For "Single Library" or "Specific Directory" you can use this token or a Library API Token instead.',
|
||||
seafileRepoTokenTip:
|
||||
'A library-scoped API token that only grants access to one specific library. ' +
|
||||
'Can be used instead of the Account API Token for "Single Library" and "Specific Directory" sync scopes.',
|
||||
seafileRepoIdTip:
|
||||
'The unique identifier (UUID) of the SeaFile library you want to synchronise. ' +
|
||||
'You can find it in your browser address bar when you open the library in the SeaFile web interface. ' +
|
||||
'Example: 7a9e1b3c-4d5f-6a7b-8c9d-0e1f2a3b4c5d. ' +
|
||||
'Required when sync scope is "Single Library" or "Specific Directory".',
|
||||
seafileSyncPathTip:
|
||||
'The absolute path of the folder to synchronise within the library specified by the Library ID above. ' +
|
||||
'Must start with a forward slash. ' +
|
||||
'All files and subfolders under this path will be included recursively. ' +
|
||||
'Example: /Documents/Reports. ' +
|
||||
'Important: The folder must exist inside the specified library. ' +
|
||||
'Paths outside the library are not supported. ' +
|
||||
'Only used when sync scope is "Specific Directory".',
|
||||
seafileIncludeSharedTip:
|
||||
'When enabled, libraries shared with you by other users will also be synced.',
|
||||
'When enabled, libraries that other users have shared with you are included in the synchronisation. ' +
|
||||
'When disabled, only libraries owned by your account are synchronised. ' +
|
||||
'Only applies when sync scope is "Entire Account".',
|
||||
seafileBatchSizeTip:
|
||||
'Number of documents to process per batch. Higher values may improve performance but use more memory. Default: 100.',
|
||||
'The number of documents processed and returned per batch during synchronisation. ' +
|
||||
'A smaller value uses less memory but may be slower overall. ' +
|
||||
'Default: 100.',
|
||||
jiraDescription:
|
||||
'Connect your Jira workspace to sync issues, comments, and attachments.',
|
||||
jiraBaseUrlTip:
|
||||
|
||||
@@ -12,6 +12,7 @@ import { IDataSourceInfoMap } from '../interface';
|
||||
import { bitbucketConstant } from './bitbucket-constant';
|
||||
import { confluenceConstant } from './confluence-constant';
|
||||
import { S3Constant } from './s3-constant';
|
||||
import { seafileConstant } from './seafile-constant';
|
||||
|
||||
export enum DataSourceKey {
|
||||
CONFLUENCE = 'confluence',
|
||||
@@ -834,39 +835,7 @@ export const DataSourceFormFields = {
|
||||
],
|
||||
},
|
||||
],
|
||||
[DataSourceKey.SEAFILE]: [
|
||||
{
|
||||
label: 'SeaFile Server URL',
|
||||
name: 'config.seafile_url',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'https://seafile.example.com',
|
||||
tooltip: t('setting.seafileUrlTip'),
|
||||
},
|
||||
{
|
||||
label: 'API Token',
|
||||
name: 'config.credentials.seafile_token',
|
||||
type: FormFieldType.Password,
|
||||
required: true,
|
||||
tooltip: t('setting.seafileTokenTip'),
|
||||
},
|
||||
{
|
||||
label: 'Include Shared Libraries',
|
||||
name: 'config.include_shared',
|
||||
type: FormFieldType.Checkbox,
|
||||
required: false,
|
||||
defaultValue: true,
|
||||
tooltip: t('setting.seafileIncludeSharedTip'),
|
||||
},
|
||||
{
|
||||
label: 'Batch Size',
|
||||
name: 'config.batch_size',
|
||||
type: FormFieldType.Number,
|
||||
required: false,
|
||||
placeholder: '100',
|
||||
tooltip: t('setting.seafileBatchSizeTip'),
|
||||
},
|
||||
],
|
||||
[DataSourceKey.SEAFILE]: seafileConstant(t),
|
||||
[DataSourceKey.MYSQL]: [
|
||||
{
|
||||
label: 'Host',
|
||||
@@ -1253,10 +1222,14 @@ export const DataSourceFormDefaultValues = {
|
||||
source: DataSourceKey.SEAFILE,
|
||||
config: {
|
||||
seafile_url: '',
|
||||
include_shared: true,
|
||||
sync_scope: 'account',
|
||||
repo_id: '',
|
||||
sync_path: '',
|
||||
include_shared: true,
|
||||
batch_size: 100,
|
||||
credentials: {
|
||||
seafile_token: '',
|
||||
seafile_token: '',
|
||||
repo_token: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import { FilterFormField, FormFieldType } from '@/components/dynamic-form';
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
export const seafileConstant = (t: TFunction) => [
|
||||
{
|
||||
label: 'SeaFile Server URL',
|
||||
name: 'config.seafile_url',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'https://seafile.example.com',
|
||||
tooltip: t('setting.seafileUrlTip'),
|
||||
},
|
||||
{
|
||||
label: 'Sync Scope',
|
||||
name: 'config.sync_scope',
|
||||
type: FormFieldType.Segmented,
|
||||
options: [
|
||||
{ label: 'Entire Account', value: 'account' },
|
||||
{ label: 'Single Library', value: 'library' },
|
||||
{ label: 'Specific Directory', value: 'directory' },
|
||||
],
|
||||
tooltip: t('setting.seafileSyncScopeTip'),
|
||||
},
|
||||
|
||||
{
|
||||
name: FilterFormField + '.account-tip',
|
||||
label: ' ',
|
||||
type: FormFieldType.Custom,
|
||||
shouldRender: (formValues: any) => {
|
||||
const scope = formValues?.config?.sync_scope ?? 'account';
|
||||
return scope === 'account';
|
||||
},
|
||||
render: () => (
|
||||
<div className="text-sm text-text-secondary bg-bg-card border border-border-button rounded-md px-3 py-2">
|
||||
{t('setting.seafileAccountScopeTip')}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Account API Token',
|
||||
name: 'config.credentials.seafile_token',
|
||||
type: FormFieldType.Password,
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
tooltip: t('setting.seafileTokenTip'),
|
||||
shouldRender: (formValues: any) => {
|
||||
const scope = formValues?.config?.sync_scope ?? 'account';
|
||||
return scope === 'account';
|
||||
},
|
||||
customValidate: (val: string, formValues: any) => {
|
||||
const scope = formValues?.config?.sync_scope ?? 'account';
|
||||
if ((!val || val.trim() === '') && scope === 'account') {
|
||||
return t('setting.seafileValidationAccountTokenRequired');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Include Shared Libraries',
|
||||
name: 'config.include_shared',
|
||||
type: FormFieldType.Checkbox,
|
||||
required: false,
|
||||
defaultValue: true,
|
||||
tooltip: t('setting.seafileIncludeSharedTip'),
|
||||
shouldRender: (formValues: any) => {
|
||||
const scope = formValues?.config?.sync_scope ?? 'account';
|
||||
return scope === 'account';
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
// Contextual info panel explaining the two-token choice
|
||||
name: FilterFormField + '.token-tip',
|
||||
label: ' ',
|
||||
type: FormFieldType.Custom,
|
||||
shouldRender: (formValues: any) => {
|
||||
const scope = formValues?.config?.sync_scope;
|
||||
return scope === 'library' || scope === 'directory';
|
||||
},
|
||||
render: () => (
|
||||
<div className="text-sm text-text-secondary bg-bg-card border border-border-button rounded-md px-3 py-2 space-y-1">
|
||||
<p className="font-medium text-text-primary">{t('setting.seafileTokenPanelHeading')}</p>
|
||||
<ul className="list-disc list-inside space-y-0.5">
|
||||
<li>
|
||||
<span className="font-medium">Account API Token</span>
|
||||
{' ' + t('setting.seafileTokenPanelAccountBullet')}
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium">Library Token</span>
|
||||
{' ' + t('setting.seafileTokenPanelLibraryBullet')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Account API Token',
|
||||
name: 'config.credentials.seafile_token',
|
||||
type: FormFieldType.Password,
|
||||
required: false,
|
||||
tooltip: t('setting.seafileTokenTip'),
|
||||
shouldRender: (formValues: any) => {
|
||||
const scope = formValues?.config?.sync_scope;
|
||||
return scope === 'library' || scope === 'directory';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Library Token',
|
||||
name: 'config.credentials.repo_token',
|
||||
type: FormFieldType.Password,
|
||||
required: false,
|
||||
tooltip: t('setting.seafileRepoTokenTip'),
|
||||
shouldRender: (formValues: any) => {
|
||||
const scope = formValues?.config?.sync_scope;
|
||||
return scope === 'library' || scope === 'directory';
|
||||
},
|
||||
customValidate: (val: string, formValues: any) => {
|
||||
const scope = formValues?.config?.sync_scope;
|
||||
const accountToken = formValues?.config?.credentials?.seafile_token;
|
||||
if (!val && !accountToken && (scope === 'library' || scope === 'directory')) {
|
||||
return t('setting.seafileValidationTokenRequired');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Library ID',
|
||||
name: 'config.repo_id',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
placeholder: 'e.g. 7a9e1b3c-4d5f-6a7b-8c9d-0e1f2a3b4c5d',
|
||||
tooltip: t('setting.seafileRepoIdTip'),
|
||||
shouldRender: (formValues: any) => {
|
||||
const scope = formValues?.config?.sync_scope;
|
||||
return scope === 'library' || scope === 'directory';
|
||||
},
|
||||
customValidate: (val: string, formValues: any) => {
|
||||
const scope = formValues?.config?.sync_scope;
|
||||
if (!val && (scope === 'library' || scope === 'directory')) {
|
||||
return t('setting.seafileValidationLibraryIdRequired');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Directory Path',
|
||||
name: 'config.sync_path',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
placeholder: '/Documents/Reports',
|
||||
tooltip: t('setting.seafileSyncPathTip'),
|
||||
shouldRender: (formValues: any) => {
|
||||
return formValues?.config?.sync_scope === 'directory';
|
||||
},
|
||||
customValidate: (val: string, formValues: any) => {
|
||||
if (!val && formValues?.config?.sync_scope === 'directory') {
|
||||
return t('setting.seafileValidationDirectoryPathRequired');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Batch Size',
|
||||
name: 'config.batch_size',
|
||||
type: FormFieldType.Number,
|
||||
required: false,
|
||||
placeholder: '100',
|
||||
tooltip: t('setting.seafileBatchSizeTip'),
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Account API Token',
|
||||
name: 'config.credentials.seafile_token',
|
||||
type: FormFieldType.Password,
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
label: 'Library Token',
|
||||
name: 'config.credentials.repo_token',
|
||||
type: FormFieldType.Password,
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
label: 'Library ID',
|
||||
name: 'config.repo_id',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
label: 'Directory Path',
|
||||
name: 'config.sync_path',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
label: 'Include Shared Libraries',
|
||||
name: 'config.include_shared',
|
||||
type: FormFieldType.Checkbox,
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user