11import configparser
22import sys
33import os
4- import uuid
54import json
6- import shutil
7- import requests
8- import subprocess
5+ import re
96
107sys .path .append (os .path .abspath (os .path .join (os .path .dirname (__file__ ), '../..' )))
118
129import click
13- from dotenv import dotenv_values , set_key ,unset_key
14- from src .services .apikey_manager import APIKeyManager
1510from src .services .product_metadata import write_product_edition
1611from src .services .settings_manager import SettingsManager
12+ from src .services .product_auth import ProductAuthService
1713from src .core .exception import CustomException
18- from src .core .config import ConfigManager
1914from src .services .appstore_sync_manager import AppStoreSyncManager
20- from src .services .integration_credentials import IntegrationCredentialProvider
2115
2216@click .group ()
2317def cli ():
2418 pass
2519
26- @cli .command ()
27- def genkey ():
28- """Generate a new API key"""
29- try :
30- key = APIKeyManager ().generate_key ()
31- click .echo (f"{ key } " )
32- except CustomException as e :
33- raise click .ClickException (e .details )
34- except Exception as e :
35- raise click .ClickException (str (e ))
36-
37- @cli .command ()
38- def getkey ():
39- """Get the API key"""
40- try :
41- key = APIKeyManager ().get_key ()
42- click .echo (f"{ key } " )
43- except CustomException as e :
44- raise click .ClickException (e .details )
45- except Exception as e :
46- raise click .ClickException (str (e ))
4720
4821@cli .command ()
4922@click .option ('--section' ,required = True , help = 'The section name' )
@@ -88,41 +61,12 @@ def getconfig(section, key):
8861 config = configparser .ConfigParser ()
8962 config .read (config_path , encoding = "utf-8" )
9063 if section is None :
91- # 返回整个配置文件内容
92- all_config = {s : dict (config .items (s )) for s in config .sections ()}
93- click .echo (json .dumps (all_config ))
94- elif key is None :
95- # 返回指定 section 的内容
96- value = dict (config .items (section )) if section in config .sections () else {}
97- click .echo (json .dumps (value ))
98- else :
99- # 返回指定 section 和 key 的内容
100- value = config .get (section , key ) if config .has_option (section , key ) else ""
101- click .echo (f"{ value } " )
102- except CustomException as e :
103- raise click .ClickException (e .details )
104- except Exception as e :
105- raise click .ClickException (str (e ))
106-
107- @cli .command ()
108- @click .option ('--section' , help = 'The section name' )
109- @click .option ('--key' , help = 'The key name' )
110- def getsysconfig (section , key ):
111- """Get a system config value or all system config as JSON"""
112- try :
113- system_config_path = os .path .abspath (os .path .join (os .path .dirname (__file__ ), '../config/system.ini' ))
114- config = configparser .ConfigParser ()
115- config .read (system_config_path , encoding = "utf-8" )
116- if section is None :
117- # 返回整个 system.ini 文件内容
11864 all_config = {s : dict (config .items (s )) for s in config .sections ()}
11965 click .echo (json .dumps (all_config ))
12066 elif key is None :
121- # 返回指定 section 的内容
12267 value = dict (config .items (section )) if section in config .sections () else {}
12368 click .echo (json .dumps (value ))
12469 else :
125- # 返回指定 section 和 key 的内容
12670 value = config .get (section , key ) if config .has_option (section , key ) else ""
12771 click .echo (f"{ value } " )
12872 except CustomException as e :
@@ -131,124 +75,15 @@ def getsysconfig(section, key):
13175 raise click .ClickException (str (e ))
13276
13377
134- @cli .command ()
135- @click .option ( '--edition' , ' edition_key' , required = True , help = 'Edition key: free | starter | standard | enterprise ' )
136- def setproductedition (edition_key ):
78+ @cli .command (hidden = True )
79+ @click .argument ( ' edition_key' )
80+ def setedition (edition_key ):
13781 """Set runtime product edition state"""
13882 try :
13983 edition = write_product_edition (edition_key )
14084 click .echo (f"Set product edition to { edition .key } (max_apps={ edition .max_apps } )" )
14185 except Exception as e :
14286 raise click .ClickException (str (e ))
143-
144- @cli .command ()
145- @click .option ('--appid' ,required = True , help = 'The App Id' )
146- @click .option ('--github_token' , required = True , help = 'The Github Token' )
147- def commit (appid , github_token ):
148- """Commit the app to the Github"""
149- try :
150- # 从配置文件读取gitea的用户名和密码
151- credentials = IntegrationCredentialProvider ().get_gitea_credentials ()
152- gitea_user = credentials .username
153- gitea_pwd = credentials .password
154-
155- # 将/tmp目录作为工作目录,如果不存在则创建,如果存在则清空
156- work_dir = "/tmp/git"
157- if os .path .exists (work_dir ):
158- shutil .rmtree (work_dir )
159- os .makedirs (work_dir )
160- os .chdir (work_dir )
161-
162- # 执行git clone命令:将gitea仓库克隆到本地
163- gitea_repo_url = f"http://{ gitea_user } :{ gitea_pwd } @websoft9-git:3000/websoft9/{ appid } .git"
164- subprocess .run (["git" , "clone" , gitea_repo_url ], check = True )
165-
166- # 执行git clone命令:将github仓库克隆到本地(dev分支)
167- github_repo_url = f"https://github.com/Websoft9/docker-library.git"
168- subprocess .run (["git" , "clone" , "--branch" , "dev" , github_repo_url ], check = True )
169-
170- # 解析gitea_repo_url下载的目录下的.env文件
171- gitea_env_path = os .path .join (work_dir , appid , '.env' )
172- gitea_env_vars = dotenv_values (gitea_env_path )
173- w9_app_name = gitea_env_vars .get ('W9_APP_NAME' )
174-
175- if not w9_app_name :
176- raise click .ClickException ("W9_APP_NAME not found in Gitea .env file" )
177-
178- # 解析github_repo_url下载的目录下的/apps/W9_APP_NAME目录下的.env文件
179- github_env_path = os .path .join (work_dir , 'docker-library' , 'apps' , w9_app_name , '.env' )
180- github_env_vars = dotenv_values (github_env_path )
181-
182- # 需要复制的变量
183- env_vars_to_copy = ['W9_URL' , 'W9_ID' ]
184- port_set_vars = {key : value for key , value in github_env_vars .items () if key .endswith ('PORT_SET' )}
185-
186- # 将这些值去替换gitea_repo_url目录下.env中对应项的值
187- for key in env_vars_to_copy :
188- if key in github_env_vars :
189- set_key (gitea_env_path , key , github_env_vars [key ])
190-
191- for key , value in port_set_vars .items ():
192- set_key (gitea_env_path , key , value )
193-
194- # 删除W9_APP_NAME
195- unset_key (gitea_env_path , 'W9_APP_NAME' )
196-
197- # 将整个gitea目录覆盖到docker-library/apps/w9_app_name目录
198- gitea_repo_dir = os .path .join (work_dir , appid )
199- github_app_dir = os .path .join (work_dir , 'docker-library' , 'apps' , w9_app_name )
200- if os .path .exists (github_app_dir ):
201- shutil .rmtree (github_app_dir )
202- shutil .copytree (gitea_repo_dir , github_app_dir )
203-
204- # 切换到docker-library目录
205- os .chdir (os .path .join (work_dir , 'docker-library' ))
206-
207- # 创建一个新的分支
208- new_branch_name = f"update-{ w9_app_name } -{ uuid .uuid4 ().hex [:8 ]} "
209- subprocess .run (["git" , "checkout" , "-b" , new_branch_name ], check = True )
210-
211- # 将修改提交到新的分支
212- subprocess .run (["git" , "add" , "." ], check = True )
213- subprocess .run (["git" , "commit" , "-m" , f"Update { w9_app_name } " ], check = True )
214-
215- # 推送新的分支到 GitHub
216- # subprocess.run(["git", "push", "origin", new_branch_name], check=True)
217-
218- # 推送新的分支到 GitHub
219- github_push_url = f"https://{ github_token } :x-oauth-basic@github.com/websoft9/docker-library.git"
220- subprocess .run (["git" , "push" , github_push_url , new_branch_name ], check = True )
221-
222- # 创建 Pull Request 使用 GitHub API
223- pr_data = {
224- "title" : f"Update { w9_app_name } " ,
225- "head" : new_branch_name ,
226- "base" : "dev" ,
227- "body" : "Automated update"
228- }
229-
230- response = requests .post (
231- f"https://api.github.com/repos/websoft9/docker-library/pulls" ,
232- headers = {
233- "Authorization" : f"token { github_token } " ,
234- "Accept" : "application/vnd.github.v3+json"
235- },
236- data = json .dumps (pr_data )
237- )
238-
239- if response .status_code != 201 :
240- raise click .ClickException (f"Failed to create Pull Request: { response .json ()} " )
241-
242- click .echo (f"Pull Request created: { response .json ().get ('html_url' )} " )
243-
244- except subprocess .CalledProcessError as e :
245- raise click .ClickException (f"Command failed: { e } " )
246- except Exception as e :
247- raise click .ClickException (str (e ))
248- finally :
249- # 删除工作目录
250- if os .path .exists (work_dir ):
251- shutil .rmtree (work_dir )
25287
25388@cli .command ()
25489@click .argument ('target' , required = True , type = click .Choice (['apps' ], case_sensitive = False ))
@@ -278,47 +113,36 @@ def upgrade(target, channel, dev, force_refresh):
278113 click .echo (f"App Store resources ({ active_channel } ) synchronized successfully." )
279114 else :
280115 click .echo (f"Unknown upgrade target: { target } " )
281- except subprocess .CalledProcessError as e :
282- raise click .ClickException (f"Upgrade command failed: { e } " )
283116 except Exception as e :
284117 raise click .ClickException (str (e ))
285118
286119
287- @cli .command (name = 'appstore-versions' )
288- def appstore_versions ():
289- """List locally available App Store dataset versions"""
120+ @cli .command (hidden = True )
121+ @click .option ('--password' , prompt = True , hide_input = True , confirmation_prompt = True , help = 'New password for the system user' )
122+ def resetpwd (password ):
123+ """Reset the Websoft9 system user password"""
290124 try :
291- result = AppStoreSyncManager ().list_versions ()
292- click .echo (json .dumps (result ))
293- except Exception as e :
294- raise click .ClickException (str (e ))
295-
296-
297- @cli .command (name = 'activate-appstore' )
298- @click .option ('--dataset-version' , required = True , help = 'Activate the specified local App Store dataset version' )
299- def activate_appstore (dataset_version ):
300- """Activate a locally available App Store dataset version"""
301- try :
302- result = AppStoreSyncManager ().activate (dataset_version = dataset_version , trigger = 'cli' )
303- click .echo (f"Activated App Store dataset version: { result .get ('datasetVersion' )} " )
304- except Exception as e :
305- raise click .ClickException (str (e ))
306-
307- @cli .command ()
308- def getallconfig ():
309- """Get all config.ini and system.ini data as JSON"""
310- try :
311- config_path = os .path .abspath (os .path .join (os .path .dirname (__file__ ), '../config/config.ini' ))
312- system_config_path = os .path .abspath (os .path .join (os .path .dirname (__file__ ), '../config/system.ini' ))
313- config = configparser .ConfigParser ()
314- system_config = configparser .ConfigParser ()
315- config .read (config_path , encoding = "utf-8" )
316- system_config .read (system_config_path , encoding = "utf-8" )
317- result = {
318- "config" : {s : dict (config .items (s )) for s in config .sections ()},
319- "system" : {s : dict (system_config .items (s )) for s in system_config .sections ()}
320- }
321- click .echo (json .dumps (result ))
125+ if len (password ) < 8 :
126+ raise click .ClickException ("Password must be at least 8 characters" )
127+ if not re .search (r"[A-Z]" , password ) or not re .search (r"[a-z]" , password ) or not re .search (r"\d" , password ) or not re .search (r"[^A-Za-z0-9]" , password ):
128+ raise click .ClickException ("Password must include uppercase, lowercase, number, and special character" )
129+
130+ auth = ProductAuthService ()
131+ system_user = auth .find_system_user ()
132+ if system_user is None :
133+ raise click .ClickException ("System user not found" )
134+
135+ username = system_user ['username' ]
136+ display_name = system_user .get ('display_name' , username )
137+ click .echo (f"\n System user: { username } ({ display_name } )" )
138+ if not click .confirm ("Reset password for this user?" ):
139+ click .echo ("Cancelled." )
140+ return
141+
142+ auth .reset_system_user_password (system_user ['id' ], password )
143+ click .echo (f"Password reset for system user '{ username } '" )
144+ except click .ClickException :
145+ raise
322146 except Exception as e :
323147 raise click .ClickException (str (e ))
324148
0 commit comments