skadd 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
skadd/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .add import *
2
+
3
+ __version__ = "1.0.0"
skadd/add.py ADDED
@@ -0,0 +1,282 @@
1
+ """
2
+ User [with Setup Key] Creating Web Tool
3
+ Dependencies: pip install [tagrepo] uvia vapp skadd
4
+ Start daemon:
5
+ export JWT_SECRET="very-strong-password"
6
+ export TOKEN_EXPIRE_MINUTES=30
7
+ export ADMIN_AUTHENTICATOR_EMAIL_LIST=EMAIL_LIST_SEPARATED_BY_COMMA
8
+ export SENDER_GMAIL_ADDRESS="YOUR_WORKING_GMAIL_ADDRESS"
9
+ export SENDER_GMAIL_APP_PWD="YOUR_WORKING_GMAIL_APP_PWD"
10
+ export FILE_AVRO_DATA="../users/setupkeys.avro"
11
+ uvia -a vapp -m skadd [-p <PORT>] [-H <HOST>]
12
+ VAPP routes:
13
+ Route: /callback -> callbackpage()
14
+ Route: /login -> loginpage()
15
+ Route: /call -> callpage()
16
+ GET -> request.query_params = {function, access_token, jsondata, module}
17
+ """
18
+
19
+ import os
20
+ import shutil
21
+ import uvicorn ## for <uvia>
22
+ from dotenv import load_dotenv
23
+ from starlette.responses import HTMLResponse
24
+ from starlette.responses import RedirectResponse
25
+ from starlette.requests import Request
26
+ from jinja2 import Template
27
+
28
+ from tagrepo import *
29
+ from skgen import check_setup_key_exist, add_setup_key, now, upper, lower
30
+ from pavro import pd, select_avro_file, read_avro_file, avro_file_insert, avro_save
31
+ from tier2 import generate_access_token, verify_access_token, text_to_email
32
+ from tier2 import create_google_random_setup_key, verify_google_code
33
+
34
+ APP_PORT = 21058
35
+ APP_DESC = 'User Creating Tool'
36
+ SESSION_USER = 'session_user'
37
+ ACCESS_TOKEN = 'user_access_token'
38
+ QUERY_FUNCTION = 'function'
39
+ FUNCTION_LOGOUT = 'logout'
40
+ FUNCTION_SELECT = 'select'
41
+ FUNCTION_INSERT = 'insert'
42
+ FUNCTION_DELETE = 'delete'
43
+ CALLBACK = 'callback'
44
+
45
+ load_dotenv()
46
+
47
+ ADMIN_AUTHENTICATOR_EMAIL_LIST = os.getenv('ADMIN_AUTHENTICATOR_EMAIL_LIST', '').split(',')
48
+ TOKEN_EXPIRE_MINUTES = float(os.getenv('TOKEN_EXPIRE_MINUTES', '30'))
49
+ JWT_SECRET = os.getenv('JWT_SECRET', 'replace-me-with-stronger-pass')
50
+
51
+ LOGIN_TITLE = os.getenv('LOGIN_TITLE') or 'User Login'
52
+ USERS_LISTING_TITLE = os.getenv('USERS_LISTING_TITLE') or 'Users Listing'
53
+ USERS_UPDATING_TITLE = os.getenv('USERS_UPDATING_TITLE') or 'Users Updating'
54
+ USERS_CREATING_TITLE = os.getenv('USERS_CREATING_TITLE') or 'Users Creating'
55
+ FORM_TITLE = os.getenv('FORM_TITLE') or 'User [with Setup Key] Creating Tool'
56
+
57
+ LABEL_INPUT_PARAMS = os.getenv('LABEL_INPUT_PARAMS') or 'User register info'
58
+ LABEL_LOGIN_PARAMS = os.getenv('LABEL_LOGIN_PARAMS') or 'Authentication info'
59
+
60
+ LABEL_USER_NAME = os.getenv('LABEL_USER_NAME') or 'User name'
61
+ LABEL_USER_EMAIL = os.getenv('LABEL_USER_EMAIL') or 'User email'
62
+ LABEL_AUTH_CODE = os.getenv('LABEL_AUTH_CODE') or 'Authenticator code'
63
+
64
+ LABEL_SUBMIT_BUTTON = os.getenv('LABEL_SUBMIT_BUTTON') or 'Create'
65
+ LABEL_CANCEL_BUTTON = os.getenv('LABEL_CANCEL_BUTTON') or 'Cancel'
66
+ LABEL_DELETE_BUTTON = os.getenv('LABEL_DELETE_BUTTON') or 'Delete'
67
+ LABEL_LOGIN_BUTTON = os.getenv('LABEL_LOGIN_BUTTON') or 'Login'
68
+
69
+ STYLE_SUBMIT_BUTTON = os.getenv('STYLE_SUBMIT_BUTTON', APP_SUBMIT_BUTTON_STYLE)
70
+ STYLE_CANCEL_BUTTON = os.getenv('STYLE_CANCEL_BUTTON', APP_CANCEL_BUTTON_STYLE)
71
+
72
+ KEY_ALREADY_EXISTS = os.getenv('KEY_ALREADY_EXISTS', 'Warning: setup key for this email already exists')
73
+ MISSING_INPUT_DATA = os.getenv('MISSING_INPUT_DATA', 'Error: missing input data')
74
+ FAILED_SAVE_DATA = os.getenv('FAILED_SAVE_DATA', 'Error: failed to save data')
75
+
76
+ YOU_ARE_LOGGED_OUT = os.getenv('YOU_ARE_LOGGED_OUT', 'You are logged out')
77
+ YOU_HAVE_TO_LOG_IN = os.getenv('YOU_HAVE_TO_LOG_IN', 'You have to log-in first')
78
+ YOU_COULD_NOT_LOG_IN = os.getenv('YOU_COULD_NOT_LOG_IN', 'Code was not verified, or user is not authorized')
79
+
80
+ YOUR_NEW_SETUP_KEY = os.getenv('YOUR_NEW_SETUP_KEY', 'New setup key')
81
+ SETUP_KEY_GUIDE = os.getenv('SETUP_KEY_GUIDE', 'Please secretly save, and add this key to your authenticator app')
82
+
83
+ STYLISH_TABLE_VIEW = os.getenv('STYLISH_TABLE_VIEW', False)
84
+ CLASSIC_TABLE_VIEW = not STYLISH_TABLE_VIEW
85
+
86
+ HEADER_SELECT_OPTION = os.getenv('HEADER_SELECT_OPTION', 'select')
87
+ FILE_AVRO_DATA = os.getenv('FILE_AVRO_DATA', './setupkeys.avro')
88
+
89
+ home_button = lambda: '&nbsp;<a title="Home" style="text-decoration:none;" href="/">&#x2302;</a>'
90
+
91
+ def insert_setup_user(email: str, name: str) -> bool:
92
+ key = str(create_google_random_setup_key())
93
+ email = lower(email)
94
+ message = f'''
95
+ {YOUR_NEW_SETUP_KEY} for {name} ({email}): {key}
96
+ {SETUP_KEY_GUIDE}
97
+ '''
98
+ send = text_to_email(message=message, recipient=email, subject=YOUR_NEW_SETUP_KEY)
99
+ if not send: return False
100
+ return add_setup_key(name=name, email=email, key=key)
101
+
102
+ def delete_setup_users(users: list, avro: str=FILE_AVRO_DATA, primarycol: str='email') -> bool:
103
+ df = read_avro_file(avro)
104
+ df = df[~df[primarycol].isin(users)]
105
+ temp = f'{avro}.temp'
106
+ trial = avro_save(df, temp)
107
+ if not trial: return False
108
+ try:
109
+ shutil.copy2(temp, avro)
110
+ return True
111
+ except:
112
+ return False
113
+
114
+ def list_all_setup_users(avro: str=FILE_AVRO_DATA, show: list=['name', 'email'], hide: list=['key', 'time'], classic: bool=CLASSIC_TABLE_VIEW, select: bool=False) -> str:
115
+ df = read_avro_file(avro)
116
+ if select:
117
+ df.insert(0, HEADER_SELECT_OPTION, [f'<input type="checkbox" name="selects" value="{df.email[i]}">' for i in range(len(df))])
118
+ if classic:
119
+ content = df.to_html(columns=show, escape=False, index=False)
120
+ else:
121
+ content = df.style.set_properties(subset=show, **{'border': '0', 'padding': '5', 'width': '100%'}).hide(subset=hide, axis='columns').to_html(escape=False, index=False)
122
+ return content
123
+
124
+ def verify_login_code(email: str, code: str, avro: str=FILE_AVRO_DATA) -> bool:
125
+ df = select_user_by_email(email=email, avro=avro)
126
+ if df is None or not len(df): return False
127
+ return verify_google_code(code, df.key[0])
128
+
129
+ def select_user_by_email(email: str, avro: str=FILE_AVRO_DATA) -> pd.DataFrame:
130
+ return select_avro_file(avro, {"all": [{"==": ["email", lower(email), lower]}]})
131
+
132
+ def homepage():
133
+ async def submit(request: Request):
134
+ return HTMLResponse(Template(APP_TMP_DIRECTORY_MENU).render(
135
+ APP_CHARSET=APP_CHARSET,
136
+ APP_VIEWPORT=APP_VIEWPORT,
137
+ APP_TITLE=APP_DESC,
138
+ FORM_TITLE=FORM_TITLE,
139
+ directory_items=[
140
+ {'href': '/login', 'name': 'Login'},
141
+ {'href': '/call?function=select', 'name': 'List Users'},
142
+ {'href': '/call?function=insert', 'name': 'Create User'},
143
+ {'href': '/call?function=delete', 'name': 'Delete User'},
144
+ {'href': '/call?function=logout', 'name': 'Logout'},
145
+ ]
146
+ ))
147
+ return submit
148
+
149
+ def callpage():
150
+ async def submit(request: Request):
151
+ func = request.query_params.get(QUERY_FUNCTION)
152
+ if func == FUNCTION_LOGOUT:
153
+ clear_session(request)
154
+ return HTMLResponse(YOU_ARE_LOGGED_OUT)
155
+ elif func == FUNCTION_SELECT:
156
+ return HTMLResponse(select_user_page(request))
157
+ elif func == FUNCTION_INSERT:
158
+ if request.method == 'GET':
159
+ return HTMLResponse(insert_user_page(request))
160
+ else:
161
+ form = await request.form()
162
+ email = form.get('email')
163
+ name = form.get('name')
164
+ insert = insert_user_page(request, email=email, name=name)
165
+ if insert == CALLBACK: return RedirectResponse(url='/call?function=select')
166
+ return HTMLResponse(insert)
167
+ elif func == FUNCTION_DELETE:
168
+ if request.method == 'GET':
169
+ return HTMLResponse(delete_user_page(request))
170
+ else:
171
+ form = await request.form()
172
+ emails = form.getlist('selects')
173
+ delete = delete_user_page(request, emails=emails)
174
+ if delete == CALLBACK: return RedirectResponse(url='/call?function=select')
175
+ return HTMLResponse(delete)
176
+ else:
177
+ return HTMLResponse('Not found')
178
+ return submit
179
+
180
+ def modify_session(request, at=None, su=None):
181
+ request.session[ACCESS_TOKEN] = at
182
+ request.session[SESSION_USER] = su
183
+
184
+ def clear_session(request):
185
+ modify_session(request, None, None)
186
+
187
+ def check_login(request) -> bool:
188
+ at = request.session.get(ACCESS_TOKEN)
189
+ su = request.session.get(SESSION_USER)
190
+ if not at: return False
191
+ if not verify_access_token(at, su):
192
+ clear_session(request)
193
+ return False
194
+ return True
195
+
196
+ def loginpage():
197
+ async def submit(request: Request):
198
+ if request.method == 'GET':
199
+ return HTMLResponse(login_page())
200
+ else:
201
+ form = await request.form()
202
+ email = form.get('email')
203
+ code = form.get('code')
204
+ if verify_login_code(email, code) and lower(email) in ADMIN_AUTHENTICATOR_EMAIL_LIST:
205
+ modify_session(request, at=generate_access_token(email), su=email)
206
+ return RedirectResponse(url='/')
207
+ else:
208
+ clear_session(request)
209
+ return HTMLResponse(YOU_COULD_NOT_LOG_IN)
210
+ return submit
211
+
212
+ def login_page() -> str:
213
+ return Template(APP_TMP_SINGLE_FORM_PAGE).render(
214
+ APP_CHARSET=APP_CHARSET,
215
+ APP_VIEWPORT=APP_VIEWPORT,
216
+ APP_TITLE=APP_DESC,
217
+ FORM_TITLE=LOGIN_TITLE+home_button(),
218
+ FORM_ACTION='/login',
219
+ FORM_INPUT_PARAMS=html_fieldset(LABEL_LOGIN_PARAMS, html_input_texts(["email", "code"], [LABEL_USER_EMAIL, LABEL_AUTH_CODE])),
220
+ STYLE_SUBMIT_BUTTON=STYLE_SUBMIT_BUTTON,
221
+ LABEL_SUBMIT_BUTTON=LABEL_LOGIN_BUTTON,
222
+ STYLE_CANCEL_BUTTON=STYLE_CANCEL_BUTTON,
223
+ LABEL_CANCEL_BUTTON=LABEL_CANCEL_BUTTON)
224
+
225
+ def users_listing_page(content: str) -> str:
226
+ return Template(APP_TMP_SIMPLE_PAGE).render(
227
+ APP_CHARSET=APP_CHARSET,
228
+ APP_VIEWPORT=APP_VIEWPORT,
229
+ APP_TITLE=APP_DESC,
230
+ FORM_TITLE=USERS_LISTING_TITLE+home_button(),
231
+ FORM_BODY=content)
232
+
233
+ def users_updating_page(content: str) -> str:
234
+ return Template(APP_TMP_SINGLE_FORM_PAGE).render(
235
+ APP_CHARSET=APP_CHARSET,
236
+ APP_VIEWPORT=APP_VIEWPORT,
237
+ APP_TITLE=APP_DESC,
238
+ FORM_TITLE=USERS_UPDATING_TITLE+home_button(),
239
+ FORM_INPUT_PARAMS=content,
240
+ STYLE_SUBMIT_BUTTON=STYLE_CANCEL_BUTTON,
241
+ LABEL_SUBMIT_BUTTON=LABEL_DELETE_BUTTON,
242
+ STYLE_CANCEL_BUTTON=STYLE_SUBMIT_BUTTON,
243
+ LABEL_CANCEL_BUTTON=LABEL_CANCEL_BUTTON)
244
+
245
+ def users_creating_page() -> str:
246
+ return Template(APP_TMP_SINGLE_FORM_PAGE).render(
247
+ APP_CHARSET=APP_CHARSET,
248
+ APP_VIEWPORT=APP_VIEWPORT,
249
+ APP_TITLE=APP_DESC,
250
+ FORM_TITLE=USERS_CREATING_TITLE+home_button(),
251
+ FORM_ACTION='/call?function=insert',
252
+ FORM_INPUT_PARAMS=html_fieldset(LABEL_INPUT_PARAMS, html_input_texts(["name", "email"], [LABEL_USER_NAME, LABEL_USER_EMAIL])),
253
+ STYLE_SUBMIT_BUTTON=STYLE_SUBMIT_BUTTON,
254
+ LABEL_SUBMIT_BUTTON=LABEL_SUBMIT_BUTTON,
255
+ STYLE_CANCEL_BUTTON=STYLE_CANCEL_BUTTON,
256
+ LABEL_CANCEL_BUTTON=LABEL_CANCEL_BUTTON)
257
+
258
+ def select_user_page(request) -> str:
259
+ if not check_login(request): return YOU_HAVE_TO_LOG_IN
260
+ return users_listing_page(list_all_setup_users())
261
+
262
+ def insert_user_page(request, email: str=None, name: str=None) -> str:
263
+ if not check_login(request): return YOU_HAVE_TO_LOG_IN
264
+ if not email and not name:
265
+ return users_creating_page()
266
+ elif not email or not name:
267
+ return MISSING_INPUT_DATA
268
+ elif check_setup_key_exist(email):
269
+ return KEY_ALREADY_EXISTS
270
+ else:
271
+ insert = insert_setup_user(email=email, name=name)
272
+ if not insert: return FAILED_SAVE_DATA
273
+ return CALLBACK
274
+
275
+ def delete_user_page(request, emails: list=None) -> str:
276
+ if not check_login(request): return YOU_HAVE_TO_LOG_IN
277
+ if not emails:
278
+ return users_updating_page(list_all_setup_users(show=[HEADER_SELECT_OPTION, 'name', 'email'], select=True))
279
+ else:
280
+ delete = delete_setup_users(emails)
281
+ if not delete: return FAILED_SAVE_DATA
282
+ return CALLBACK
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: skadd
3
+ Version: 1.0.0
4
+ Summary: User [with Setup Key] Creating Tool
5
+ Home-page: https://github.com/asinerum/skadd
6
+ Author: Asinerum Conlang Project
7
+ Author-email: asinerum.com@gmail.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.7
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: python-dotenv>=1.2.1
16
+ Requires-Dist: python-multipart>=0.0.29
17
+ Requires-Dist: starlette>=0.50.0
18
+ Requires-Dist: uvicorn>=0.38.0
19
+ Requires-Dist: jinja2>=3.1.6
20
+ Requires-Dist: tagrepo>=1.0.5
21
+ Requires-Dist: tier2>=1.0.6
22
+ Requires-Dist: pavro>=1.0.2
23
+ Requires-Dist: skgen>=1.0.2
24
+ Requires-Dist: uvia>=1.0.2
25
+ Requires-Dist: vapp>=1.0.4
26
+ Dynamic: license-file
27
+
28
+ # User [with Setup Key] Creating Tool
29
+
30
+ ## Purpose
31
+ - This tiny tool (working as a web daemon) helps people to manage their small community of users.
32
+ - The users are created manually by an administrator, and about to receive emails with their secret setup keys which can be added to modern authenticator apps.
33
+ - After being created, a new user can log in the community portal with TOTP authenticator code.
34
+ - The tool includes: user listing, creating, deleting routines. The user-creating routine allows administrators add new user name and email, then automatically send notification email.
35
+ - The automatic email sending uses Google mail API under administrators Gmail account.
36
+
37
+ ## Installation
38
+ ```bash
39
+ pip install skadd
40
+ ```
41
+
42
+ ## Sample Usage
43
+ ```bash
44
+ ## nano skadd.sh
45
+ export JWT_SECRET="very-strong-password"
46
+ export TOKEN_EXPIRE_MINUTES=30
47
+ export ADMIN_AUTHENTICATOR_EMAIL_LIST="EMAIL_LIST_SEPARATED_BY_COMMA"
48
+ export SENDER_GMAIL_ADDRESS="YOUR_WORKING_GMAIL_ADDRESS"
49
+ export SENDER_GMAIL_APP_PWD="YOUR_WORKING_GMAIL_APP_PWD"
50
+ export FILE_AVRO_DATA="./users/setupkeys.avro"
51
+ uvia -a vapp -m skadd [-p <PORT> [-H <HOST>]]
52
+ ## The <PORT> is 21058 by default, but can be edited
53
+ ## The <HOST> should be 127.0.0.1 for secutiry reason
54
+ ## chmod +x skadd.sh
55
+ ## ./skadd.sh
56
+ ```
57
+
58
+ Detailed tips, tricks, and examples, can be found at project's repository
59
+ https://github.com/asinerum/skadd
60
+
61
+ (C) 2026 Asinerum Conlang Project
@@ -0,0 +1,7 @@
1
+ skadd/__init__.py,sha256=_b7ixK7mStMV1ih0Jho37A45XXT5gn1YrcpuhdXWQdE,45
2
+ skadd/add.py,sha256=z9qbFxu6i8zfnZMWrRg0VmZW98pA1H6vxvud0B8p1nA,11469
3
+ skadd-1.0.0.dist-info/licenses/LICENSE,sha256=4npUbkrpgB6lqMiYYeUxZAP4SOkjVSwK8-7jW60mxvw,1081
4
+ skadd-1.0.0.dist-info/METADATA,sha256=PsmmplC7Ssy9BDqXRWsx8R9K1yifwuumasKraL3VO5w,2288
5
+ skadd-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ skadd-1.0.0.dist-info/top_level.txt,sha256=56rwI2vQF-EOfUD9PaDA_MjRgBhqxaEDEbQCd8XfgIg,6
7
+ skadd-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Asinerum Conlang Project
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ skadd