Skip to content

Commit 806ed9d

Browse files
authored
feat: Implement authorization mode for wiki generation (#185)
This commit introduces an authorization mode for the wiki generation feature. When enabled via environment variables, you must provide a valid authorization code to generate a wiki. Key changes: - Backend: - Added `DEEPWIKI_AUTH_MODE` and `DEEPWIKI_AUTH_CODE` environment variables to control the feature. - Created an `/auth/status` endpoint to inform the frontend if auth is required. - Created an `/auth/validate` endpoint for validating the authorization code if auth is required. - Updated the Delete Wiki Cache request parameters to include an `auth_code`. - Implemented an authorization check in the Delete Wiki Cache handler to validate the `auth_code` if auth mode is active. - Frontend: - The main page now fetches the auth status from the backend. - The configuration modal and the model selection modal conditionally display an input field for the authorization code if required. - The `auth_code` is sent to the backend for checking before the wiki generation request and during the wiki cache deletion. - Documentation: - `README.md` and `README.zh.md` have been updated to explain the new environment variables and the authorization feature. This allows administrators to restrict wiki generation capabilities to authorized users in the front end, but not prevent the redirect to backend generate interface. This is becauce that it hard to make diffrences the request is for asking or generating, and the more important thing for administrators is to protect the pages which have generated, but not prevent user to generate the own page. So this can not prevent user to generate his own page completely but is aimed to protect the pages generated by administrators.
1 parent eb6688d commit 806ed9d

File tree

17 files changed

+294
-21
lines changed

17 files changed

+294
-21
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,9 +303,23 @@ docker-compose up
303303
| `OLLAMA_HOST` | Ollama Host (default: http://localhost:11434) | No | Required only if you want to use external Ollama server |
304304
| `PORT` | Port for the API server (default: 8001) | No | If you host API and frontend on the same machine, make sure change port of `SERVER_BASE_URL` accordingly |
305305
| `SERVER_BASE_URL` | Base URL for the API server (default: http://localhost:8001) | No |
306+
| `DEEPWIKI_AUTH_MODE` | Set to `true` or `1` to enable authorization mode. | No | Defaults to `false`. If enabled, `DEEPWIKI_AUTH_CODE` is required. |
307+
| `DEEPWIKI_AUTH_CODE` | The secret code required for wiki generation when `DEEPWIKI_AUTH_MODE` is enabled. | No | Only used if `DEEPWIKI_AUTH_MODE` is `true` or `1`. |
306308

307309
If you're not using ollama mode, you need to configure an OpenAI API key for embeddings. Other API keys are only required when configuring and using models from the corresponding providers.
308310

311+
## Authorization Mode
312+
313+
DeepWiki can be configured to run in an authorization mode, where wiki generation requires a valid authorization code. This is useful if you want to control who can use the generation feature.
314+
Restricts frontend initiation and protects cache deletion, but doesn't fully prevent backend generation if API endpoints are hit directly.
315+
316+
To enable authorization mode, set the following environment variables:
317+
318+
- `DEEPWIKI_AUTH_MODE`: Set this to `true` or `1`. When enabled, the frontend will display an input field for the authorization code.
319+
- `DEEPWIKI_AUTH_CODE`: Set this to the desired secret code. Restricts frontend initiation and protects cache deletion, but doesn't fully prevent backend generation if API endpoints are hit directly.
320+
321+
If `DEEPWIKI_AUTH_MODE` is not set or is set to `false` (or any other value than `true`/`1`), the authorization feature will be disabled, and no code will be required.
322+
309323
### Docker Setup
310324

311325
You can use Docker to run DeepWiki:

README.zh.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,9 +310,25 @@ OPENAI_BASE_URL=https://custom-api-endpoint.com/v1 # 可选,用于自定义Op
310310
311311
# 配置目录
312312
DEEPWIKI_CONFIG_DIR=/path/to/custom/config/dir # 可选,用于自定义配置文件位置
313+
314+
# 授权模式
315+
DEEPWIKI_AUTH_MODE=true # 设置为 true 或 1 以启用授权模式
316+
DEEPWIKI_AUTH_CODE=your_secret_code # 当 DEEPWIKI_AUTH_MODE 启用时所需的授权码
313317
```
314318
如果不使用ollama模式,那么需要配置OpenAI API密钥用于embeddings。其他密钥只有配置并使用使用对应提供商的模型时才需要。
315319

320+
## 授权模式
321+
322+
DeepWiki 可以配置为在授权模式下运行,在该模式下,生成 Wiki 需要有效的授权码。如果您想控制谁可以使用生成功能,这将非常有用。
323+
限制使用前端页面生成wiki并保护已生成页面的缓存删除,但无法完全阻止直接访问 API 端点生成wiki。主要目的是为了保护管理员已生成的wiki页面,防止被访问者重新生成。
324+
325+
要启用授权模式,请设置以下环境变量:
326+
327+
- `DEEPWIKI_AUTH_MODE`: 将此设置为 `true``1`。启用后,前端将显示一个用于输入授权码的字段。
328+
- `DEEPWIKI_AUTH_CODE`: 将此设置为所需的密钥。限制使用前端页面生成wiki并保护已生成页面的缓存删除,但无法完全阻止直接访问 API 端点生成wiki。
329+
330+
如果未设置 `DEEPWIKI_AUTH_MODE` 或将其设置为 `false`(或除 `true`/`1` 之外的任何其他值),则授权功能将被禁用,并且不需要任何代码。
331+
316332
### 配置文件
317333

318334
DeepWiki使用JSON配置文件管理系统的各个方面:

api/api.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,24 @@ class ModelConfig(BaseModel):
123123
providers: List[Provider] = Field(..., description="List of available model providers")
124124
defaultProvider: str = Field(..., description="ID of the default provider")
125125

126-
from api.config import configs
126+
class AuthorizationConfig(BaseModel):
127+
code: str = Field(..., description="Authorization code")
128+
129+
from api.config import configs, WIKI_AUTH_MODE, WIKI_AUTH_CODE
130+
131+
@app.get("/auth/status")
132+
async def get_auth_status():
133+
"""
134+
Check if authentication is required for the wiki.
135+
"""
136+
return {"auth_required": WIKI_AUTH_MODE}
137+
138+
@app.post("/auth/validate")
139+
async def validate_auth_code(request: AuthorizationConfig):
140+
"""
141+
Check authorization code.
142+
"""
143+
return {"success": WIKI_AUTH_CODE == request.code}
127144

128145
@app.get("/models/config", response_model=ModelConfig)
129146
async def get_model_config():
@@ -454,11 +471,17 @@ async def delete_wiki_cache(
454471
owner: str = Query(..., description="Repository owner"),
455472
repo: str = Query(..., description="Repository name"),
456473
repo_type: str = Query(..., description="Repository type (e.g., github, gitlab)"),
457-
language: str = Query(..., description="Language of the wiki content")
474+
language: str = Query(..., description="Language of the wiki content"),
475+
authorization_code: str = Query(..., description="Authorization code")
458476
):
459477
"""
460478
Deletes a specific wiki cache from the file system.
461479
"""
480+
if WIKI_AUTH_MODE:
481+
logger.info("check the authorization code")
482+
if WIKI_AUTH_CODE != authorization_code:
483+
raise HTTPException(status_code=401, detail="Authorization code is invalid")
484+
462485
logger.info(f"Attempting to delete wiki cache for {owner}/{repo} ({repo_type}), lang: {language}")
463486
cache_path = get_wiki_cache_path(owner, repo, repo_type, language)
464487

@@ -497,7 +520,9 @@ async def root():
497520
"Wiki": [
498521
"POST /export/wiki - Export wiki content as Markdown or JSON",
499522
"GET /api/wiki_cache - Retrieve cached wiki data",
500-
"POST /api/wiki_cache - Store wiki data to cache"
523+
"POST /api/wiki_cache - Store wiki data to cache",
524+
"GET /auth/status - Check if wiki authentication is enabled",
525+
"POST /auth/validate - Check if wiki authentication is valid"
501526
],
502527
"LocalRepo": [
503528
"GET /local_repo/structure - Get structure of a local repository (with path parameter)",

api/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
if AWS_ROLE_ARN:
3838
os.environ["AWS_ROLE_ARN"] = AWS_ROLE_ARN
3939

40+
# Wiki authentication settings
41+
raw_auth_mode = os.environ.get('DEEPWIKI_AUTH_MODE', 'False')
42+
WIKI_AUTH_MODE = raw_auth_mode.lower() in ['true', '1', 't']
43+
WIKI_AUTH_CODE = os.environ.get('DEEPWIKI_AUTH_CODE', '')
44+
4045
# Get configuration directory from environment variable, or use default if not set
4146
CONFIG_DIR = os.environ.get('DEEPWIKI_CONFIG_DIR', None)
4247

next.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ const nextConfig: NextConfig = {
5151
source: '/local_repo/structure',
5252
destination: `${TARGET_SERVER_BASE_URL}/local_repo/structure`,
5353
},
54+
{
55+
source: '/api/auth/status',
56+
destination: `${TARGET_SERVER_BASE_URL}/auth/status`,
57+
},
58+
{
59+
source: '/api/auth/validate',
60+
destination: `${TARGET_SERVER_BASE_URL}/auth/validate`,
61+
},
5462
];
5563
},
5664
};

src/app/[owner]/[repo]/page.tsx

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,11 @@ export default function RepoWikiPage() {
245245
const [isAskModalOpen, setIsAskModalOpen] = useState(false);
246246
const askComponentRef = useRef<{ clearConversation: () => void } | null>(null);
247247

248+
// Authentication state
249+
const [authRequired, setAuthRequired] = useState<boolean>(false);
250+
const [authCode, setAuthCode] = useState<string>('');
251+
const [isAuthLoading, setIsAuthLoading] = useState<boolean>(true);
252+
248253
// Memoize repo info to avoid triggering updates in callbacks
249254

250255
// Add useEffect to handle scroll reset
@@ -256,6 +261,29 @@ export default function RepoWikiPage() {
256261
}
257262
}, [currentPageId]);
258263

264+
// Fetch authentication status on component mount
265+
useEffect(() => {
266+
const fetchAuthStatus = async () => {
267+
try {
268+
setIsAuthLoading(true);
269+
const response = await fetch('/api/auth/status');
270+
if (!response.ok) {
271+
throw new Error(`HTTP error! status: ${response.status}`);
272+
}
273+
const data = await response.json();
274+
setAuthRequired(data.auth_required);
275+
} catch (err) {
276+
console.error("Failed to fetch auth status:", err);
277+
// Assuming auth is required if fetch fails to avoid blocking UI for safety
278+
setAuthRequired(true);
279+
} finally {
280+
setIsAuthLoading(false);
281+
}
282+
};
283+
284+
fetchAuthStatus();
285+
}, []);
286+
259287
// Generate content for a wiki page
260288
const generatePageContent = useCallback(async (page: WikiPage, owner: string, repo: string) => {
261289
return new Promise<void>(async (resolve) => {
@@ -1378,6 +1406,7 @@ IMPORTANT:
13781406
is_custom_model: isCustomSelectedModelState.toString(),
13791407
custom_model: customSelectedModelState,
13801408
comprehensive: isComprehensiveView.toString(),
1409+
authorization_code: authCode,
13811410
});
13821411

13831412
// Add file filters configuration
@@ -1387,8 +1416,17 @@ IMPORTANT:
13871416
if (modelExcludedFiles) {
13881417
params.append('excluded_files', modelExcludedFiles);
13891418
}
1419+
1420+
if(authRequired && !authCode) {
1421+
console.warn("Authorization code is required");
1422+
return;
1423+
}
1424+
13901425
const response = await fetch(`/api/wiki_cache?${params.toString()}`, {
13911426
method: 'DELETE',
1427+
headers: {
1428+
'Accept': 'application/json',
1429+
}
13921430
});
13931431

13941432
if (response.ok) {
@@ -1400,11 +1438,17 @@ IMPORTANT:
14001438
console.warn(`Failed to clear server-side wiki cache (status: ${response.status}): ${errorText}. Proceeding with refresh anyway.`);
14011439
// Optionally, inform the user about the cache clear failure but that refresh will still attempt
14021440
// setError(\`Cache clear failed: ${errorText}. Trying to refresh...\`);
1441+
if(response.status == 401) {
1442+
setIsLoading(false);
1443+
throw new Error('Failed to validate the authorization code');
1444+
}
14031445
}
14041446
} catch (err) {
14051447
console.warn('Error calling DELETE /api/wiki_cache:', err);
1448+
setIsLoading(false);
14061449
// Optionally, inform the user about the cache clear error
14071450
// setError(\`Error clearing cache: ${err instanceof Error ? err.message : String(err)}. Trying to refresh...\`);
1451+
throw err;
14081452
}
14091453

14101454
// Update token if provided
@@ -1451,7 +1495,7 @@ IMPORTANT:
14511495
// For now, we rely on the standard loadData flow initiated by resetting effectRan and dependencies.
14521496
// This will re-trigger the main data loading useEffect.
14531497
// No direct call to fetchRepositoryStructure here, let the useEffect handle it based on effectRan.current = false.
1454-
}, [effectiveRepoInfo, language, messages.loading, activeContentRequests, selectedProviderState, selectedModelState, isCustomSelectedModelState, customSelectedModelState, modelExcludedDirs, modelExcludedFiles, isComprehensiveView]);
1498+
}, [effectiveRepoInfo, language, messages.loading, activeContentRequests, selectedProviderState, selectedModelState, isCustomSelectedModelState, customSelectedModelState, modelExcludedDirs, modelExcludedFiles, isComprehensiveView, authCode, authRequired]);
14551499

14561500
// Start wiki generation when component mounts
14571501
useEffect(() => {
@@ -1632,7 +1676,7 @@ IMPORTANT:
16321676

16331677
// Clean up function for this effect is not strictly necessary for loadData,
16341678
// but keeping the main unmount cleanup in the other useEffect
1635-
}, [effectiveRepoInfo.owner, effectiveRepoInfo.repo, effectiveRepoInfo.type, language, fetchRepositoryStructure, messages.loading?.fetchingCache, isComprehensiveView]);
1679+
}, [effectiveRepoInfo, effectiveRepoInfo.owner, effectiveRepoInfo.repo, effectiveRepoInfo.type, language, fetchRepositoryStructure, messages.loading?.fetchingCache, isComprehensiveView]);
16361680

16371681
// Save wiki to server-side cache when generation is complete
16381682
useEffect(() => {
@@ -1689,7 +1733,7 @@ IMPORTANT:
16891733
};
16901734

16911735
saveCache();
1692-
}, [isLoading, error, wikiStructure, generatedPages, effectiveRepoInfo.owner, effectiveRepoInfo.repo, effectiveRepoInfo.type, language, isComprehensiveView]);
1736+
}, [isLoading, error, wikiStructure, generatedPages, effectiveRepoInfo.owner, effectiveRepoInfo.repo, effectiveRepoInfo.type, effectiveRepoInfo.repoUrl, repoUrl, language, isComprehensiveView]);
16931737

16941738
const handlePageSelect = (pageId: string) => {
16951739
if (currentPageId != pageId) {
@@ -2023,6 +2067,10 @@ IMPORTANT:
20232067
showWikiType={true}
20242068
showTokenInput={effectiveRepoInfo.type !== 'local' && !currentToken} // Show token input if not local and no current token
20252069
repositoryType={effectiveRepoInfo.type as 'github' | 'gitlab' | 'bitbucket'}
2070+
authRequired={authRequired}
2071+
authCode={authCode}
2072+
setAuthCode={setAuthCode}
2073+
isAuthLoading={isAuthLoading}
20262074
/>
20272075
</div>
20282076
);

src/app/api/wiki/projects/route.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ interface DeleteProjectCachePayload {
1919
}
2020

2121
/** Type guard to validate DeleteProjectCachePayload at runtime */
22-
function isDeleteProjectCachePayload(obj: any): obj is DeleteProjectCachePayload {
22+
function isDeleteProjectCachePayload(obj: unknown): obj is DeleteProjectCachePayload {
2323
return (
2424
obj != null &&
25-
typeof obj.owner === 'string' && obj.owner.trim() !== '' &&
26-
typeof obj.repo === 'string' && obj.repo.trim() !== '' &&
27-
typeof obj.repo_type === 'string' && obj.repo_type.trim() !== '' &&
28-
typeof obj.language === 'string' && obj.language.trim() !== ''
25+
typeof obj === 'object' &&
26+
'owner' in obj && typeof (obj as Record<string, unknown>).owner === 'string' && ((obj as Record<string, unknown>).owner as string).trim() !== '' &&
27+
'repo' in obj && typeof (obj as Record<string, unknown>).repo === 'string' && ((obj as Record<string, unknown>).repo as string).trim() !== '' &&
28+
'repo_type' in obj && typeof (obj as Record<string, unknown>).repo_type === 'string' && ((obj as Record<string, unknown>).repo_type as string).trim() !== '' &&
29+
'language' in obj && typeof (obj as Record<string, unknown>).language === 'string' && ((obj as Record<string, unknown>).language as string).trim() !== ''
2930
);
3031
}
3132

src/app/page.tsx

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,39 @@ export default function Home() {
9696
const [isSubmitting, setIsSubmitting] = useState(false);
9797
const [selectedLanguage, setSelectedLanguage] = useState<string>(language);
9898

99+
// Authentication state
100+
const [authRequired, setAuthRequired] = useState<boolean>(false);
101+
const [authCode, setAuthCode] = useState<string>('');
102+
const [isAuthLoading, setIsAuthLoading] = useState<boolean>(true);
103+
99104
// Sync the language context with the selectedLanguage state
100105
useEffect(() => {
101106
setLanguage(selectedLanguage);
102107
}, [selectedLanguage, setLanguage]);
103108

109+
// Fetch authentication status on component mount
110+
useEffect(() => {
111+
const fetchAuthStatus = async () => {
112+
try {
113+
setIsAuthLoading(true);
114+
const response = await fetch('/api/auth/status');
115+
if (!response.ok) {
116+
throw new Error(`HTTP error! status: ${response.status}`);
117+
}
118+
const data = await response.json();
119+
setAuthRequired(data.auth_required);
120+
} catch (err) {
121+
console.error("Failed to fetch auth status:", err);
122+
// Assuming auth is required if fetch fails to avoid blocking UI for safety
123+
setAuthRequired(true);
124+
} finally {
125+
setIsAuthLoading(false);
126+
}
127+
};
128+
129+
fetchAuthStatus();
130+
}, []);
131+
104132
// Parse repository URL/input and extract owner and repo
105133
const parseRepositoryInput = (input: string): {
106134
owner: string,
@@ -181,7 +209,39 @@ export default function Home() {
181209
setIsConfigModalOpen(true);
182210
};
183211

184-
const handleGenerateWiki = () => {
212+
const validateAuthCode = async () => {
213+
try {
214+
if(authRequired) {
215+
if(!authCode) {
216+
return false;
217+
}
218+
const response = await fetch('/api/auth/validate', {
219+
method: 'POST',
220+
headers: {
221+
'Content-Type': 'application/json',
222+
},
223+
body: JSON.stringify({'code': authCode})
224+
});
225+
if (!response.ok) {
226+
return false;
227+
}
228+
const data = await response.json();
229+
return data.success || false;
230+
}
231+
} catch {
232+
return false;
233+
}
234+
return true;
235+
};
236+
237+
const handleGenerateWiki = async () => {
238+
239+
// Check authorization code
240+
const validation = await validateAuthCode();
241+
if(!validation) {
242+
throw new Error('Failed to validate the authorization code');
243+
}
244+
185245
// Prevent multiple submissions
186246
if (isSubmitting) {
187247
console.log('Form submission already in progress, ignoring duplicate click');
@@ -329,6 +389,10 @@ export default function Home() {
329389
setIncludedFiles={setIncludedFiles}
330390
onSubmit={handleGenerateWiki}
331391
isSubmitting={isSubmitting}
392+
authRequired={authRequired}
393+
authCode={authCode}
394+
setAuthCode={setAuthCode}
395+
isAuthLoading={isAuthLoading}
332396
/>
333397

334398
</div>

src/components/Ask.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,8 @@ const Ask: React.FC<AskProps> = ({
882882
console.log('Model selection applied:', selectedProvider, selectedModel);
883883
}}
884884
showWikiType={false}
885+
authRequired={false}
886+
isAuthLoading={false}
885887
/>
886888
</div>
887889
);

0 commit comments

Comments
 (0)