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 +3 -0
- skadd/add.py +282 -0
- skadd-1.0.0.dist-info/METADATA +61 -0
- skadd-1.0.0.dist-info/RECORD +7 -0
- skadd-1.0.0.dist-info/WHEEL +5 -0
- skadd-1.0.0.dist-info/licenses/LICENSE +21 -0
- skadd-1.0.0.dist-info/top_level.txt +1 -0
skadd/__init__.py
ADDED
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: ' <a title="Home" style="text-decoration:none;" href="/">⌂</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,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
|