usethatapp 0.1.0__tar.gz
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.
- usethatapp-0.1.0/LICENSE +21 -0
- usethatapp-0.1.0/PKG-INFO +142 -0
- usethatapp-0.1.0/README.md +126 -0
- usethatapp-0.1.0/pyproject.toml +24 -0
- usethatapp-0.1.0/setup.cfg +4 -0
- usethatapp-0.1.0/usethatapp/__init__.py +0 -0
- usethatapp-0.1.0/usethatapp/encryption.py +79 -0
- usethatapp-0.1.0/usethatapp/webapps.py +97 -0
- usethatapp-0.1.0/usethatapp.egg-info/PKG-INFO +142 -0
- usethatapp-0.1.0/usethatapp.egg-info/SOURCES.txt +11 -0
- usethatapp-0.1.0/usethatapp.egg-info/dependency_links.txt +1 -0
- usethatapp-0.1.0/usethatapp.egg-info/requires.txt +1 -0
- usethatapp-0.1.0/usethatapp.egg-info/top_level.txt +1 -0
usethatapp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 UseThatApp
|
|
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,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: usethatapp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: For use in your UseThatApp Web Apps
|
|
5
|
+
Author-email: UseThatApp <support@usethatapp.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/UseThatApp/python
|
|
8
|
+
Keywords: UseThatApp,Python,Web Apps
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: cryptography==46.0.5
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# Summary
|
|
18
|
+
Use this package with your UseThatApp WebApps to acquire user license info from UseThatApp.com.
|
|
19
|
+
|
|
20
|
+
# Usage
|
|
21
|
+
|
|
22
|
+
## Single Page Dash App
|
|
23
|
+
In a simple single page Dash-Plotly application.
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from dash import Dash, dcc, html, Input, Output, ClientsideFunction
|
|
27
|
+
from usethatapp.webapps import get_product
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
app = Dash(__name__, external_scripts=[
|
|
31
|
+
"https://cdn.jsdelivr.net/gh/UseThatApp/cdn@latest/usethatapp.js"
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
app.layout = html.Div([
|
|
35
|
+
dcc.Location(id='url', refresh=False),
|
|
36
|
+
html.Div(id='content'),
|
|
37
|
+
dcc.Store(id='access-level-store', data=None, storage_type='memory'),
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
app.clientside_callback(
|
|
41
|
+
ClientsideFunction(namespace='clientside', function_name='requestAccessLevel'),
|
|
42
|
+
Output("access-level-store", "data"),
|
|
43
|
+
Input('url', 'pathname')
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@app.callback(
|
|
47
|
+
Output('content', 'children'),
|
|
48
|
+
Input("access-level-store", "data")
|
|
49
|
+
)
|
|
50
|
+
def display_content_based_on_access(data):
|
|
51
|
+
try:
|
|
52
|
+
product = get_product(
|
|
53
|
+
data['message'],
|
|
54
|
+
public_key_path=r'/Path/To/File/Containing/UseThatApp_Public_Key',
|
|
55
|
+
private_key_path=r'/Path/To/File/Containing/My_UTA_Private_Key'
|
|
56
|
+
)
|
|
57
|
+
# display content based on licensed plan
|
|
58
|
+
if product == 'Pro':
|
|
59
|
+
return html.Div([html.H1(["Paid Content"])])
|
|
60
|
+
except Exception as e:
|
|
61
|
+
# If there's an error (e.g. clientside hasn't populated the store yet), fall back to free content
|
|
62
|
+
app.logger.exception("Error determining product: %s", e)
|
|
63
|
+
return html.Div([html.H1(["Free Content"])])
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Mult-Page Dash App
|
|
67
|
+
In a multi-page Dash-Plotly application (using Dash Pages). The pattern below mirrors the single-page example: the main `app` registers a clientside callback that places the UseThatApp message into a `dcc.Store`, and each page reads that store (via a callback) and calls `get_product` to decide what to render.
|
|
68
|
+
|
|
69
|
+
### app.py
|
|
70
|
+
```python
|
|
71
|
+
import dash
|
|
72
|
+
from dash import Dash, html, dcc, Input, Output, ClientsideFunction
|
|
73
|
+
|
|
74
|
+
APP_TITLE = "Dash App"
|
|
75
|
+
|
|
76
|
+
app = Dash(
|
|
77
|
+
__name__,
|
|
78
|
+
title=APP_TITLE,
|
|
79
|
+
use_pages=True, # Allows us to register pages
|
|
80
|
+
external_scripts=["https://cdn.jsdelivr.net/gh/UseThatApp/cdn@latest/usethatapp.js"],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Top-level layout includes Location, a Store for the access payload, and the page container
|
|
84
|
+
app.layout = html.Div([
|
|
85
|
+
dcc.Location(id='url', refresh=False),
|
|
86
|
+
dcc.Store(id='access-level-store', data=None, storage_type='memory'),
|
|
87
|
+
dash.page_container,
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
# Register the same clientside callback used in the single-page example to populate the store
|
|
91
|
+
app.clientside_callback(
|
|
92
|
+
ClientsideFunction(namespace='clientside', function_name='requestAccessLevel'),
|
|
93
|
+
Output('access-level-store', 'data'),
|
|
94
|
+
Input('url', 'pathname')
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
server = app.server
|
|
98
|
+
|
|
99
|
+
if __name__ == '__main__':
|
|
100
|
+
app.run_server(debug=True)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### pages/home.py
|
|
104
|
+
```python
|
|
105
|
+
from dash import html, register_page, Input, Output, dash
|
|
106
|
+
from usethatapp.webapps import get_product
|
|
107
|
+
|
|
108
|
+
register_page(
|
|
109
|
+
__name__,
|
|
110
|
+
name='Home',
|
|
111
|
+
path='/'
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Page layout is a placeholder container which we'll populate via a callback that
|
|
115
|
+
# listens to the shared `access-level-store` placed by the clientside JS.
|
|
116
|
+
def layout(**url_vars):
|
|
117
|
+
return html.Div(id='home-content')
|
|
118
|
+
|
|
119
|
+
# Use dash.callback (so the callback is registered on the global app when pages are loaded)
|
|
120
|
+
@dash.callback(Output('home-content', 'children'), Input('access-level-store', 'data'))
|
|
121
|
+
def display_content_based_on_access(data):
|
|
122
|
+
# Use the helper function to parse the message and determine the licensed product
|
|
123
|
+
try:
|
|
124
|
+
product = get_product(
|
|
125
|
+
data['message'],
|
|
126
|
+
public_key_path=r'/Path/To/File/Containing/UseThatApp_Public_Key',
|
|
127
|
+
private_key_path=r'/Path/To/File/Containing/My_UTA_Private_Key'
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if product == 'Pro':
|
|
131
|
+
return html.Div([html.H1(["Paid Content"])])
|
|
132
|
+
except Exception as e:
|
|
133
|
+
# If there's an error (e.g. clientside hasn't populated the store yet), fall back to free content
|
|
134
|
+
dash.get_app().server.logger.exception("Error determining product: %s", e)
|
|
135
|
+
return html.Div([html.H1(["Free Content"])])
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
# Change Log
|
|
139
|
+
|
|
140
|
+
## Version 0.1.0 (03/19/2026)
|
|
141
|
+
- Initial release
|
|
142
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Summary
|
|
2
|
+
Use this package with your UseThatApp WebApps to acquire user license info from UseThatApp.com.
|
|
3
|
+
|
|
4
|
+
# Usage
|
|
5
|
+
|
|
6
|
+
## Single Page Dash App
|
|
7
|
+
In a simple single page Dash-Plotly application.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from dash import Dash, dcc, html, Input, Output, ClientsideFunction
|
|
11
|
+
from usethatapp.webapps import get_product
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
app = Dash(__name__, external_scripts=[
|
|
15
|
+
"https://cdn.jsdelivr.net/gh/UseThatApp/cdn@latest/usethatapp.js"
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
app.layout = html.Div([
|
|
19
|
+
dcc.Location(id='url', refresh=False),
|
|
20
|
+
html.Div(id='content'),
|
|
21
|
+
dcc.Store(id='access-level-store', data=None, storage_type='memory'),
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
app.clientside_callback(
|
|
25
|
+
ClientsideFunction(namespace='clientside', function_name='requestAccessLevel'),
|
|
26
|
+
Output("access-level-store", "data"),
|
|
27
|
+
Input('url', 'pathname')
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
@app.callback(
|
|
31
|
+
Output('content', 'children'),
|
|
32
|
+
Input("access-level-store", "data")
|
|
33
|
+
)
|
|
34
|
+
def display_content_based_on_access(data):
|
|
35
|
+
try:
|
|
36
|
+
product = get_product(
|
|
37
|
+
data['message'],
|
|
38
|
+
public_key_path=r'/Path/To/File/Containing/UseThatApp_Public_Key',
|
|
39
|
+
private_key_path=r'/Path/To/File/Containing/My_UTA_Private_Key'
|
|
40
|
+
)
|
|
41
|
+
# display content based on licensed plan
|
|
42
|
+
if product == 'Pro':
|
|
43
|
+
return html.Div([html.H1(["Paid Content"])])
|
|
44
|
+
except Exception as e:
|
|
45
|
+
# If there's an error (e.g. clientside hasn't populated the store yet), fall back to free content
|
|
46
|
+
app.logger.exception("Error determining product: %s", e)
|
|
47
|
+
return html.Div([html.H1(["Free Content"])])
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Mult-Page Dash App
|
|
51
|
+
In a multi-page Dash-Plotly application (using Dash Pages). The pattern below mirrors the single-page example: the main `app` registers a clientside callback that places the UseThatApp message into a `dcc.Store`, and each page reads that store (via a callback) and calls `get_product` to decide what to render.
|
|
52
|
+
|
|
53
|
+
### app.py
|
|
54
|
+
```python
|
|
55
|
+
import dash
|
|
56
|
+
from dash import Dash, html, dcc, Input, Output, ClientsideFunction
|
|
57
|
+
|
|
58
|
+
APP_TITLE = "Dash App"
|
|
59
|
+
|
|
60
|
+
app = Dash(
|
|
61
|
+
__name__,
|
|
62
|
+
title=APP_TITLE,
|
|
63
|
+
use_pages=True, # Allows us to register pages
|
|
64
|
+
external_scripts=["https://cdn.jsdelivr.net/gh/UseThatApp/cdn@latest/usethatapp.js"],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Top-level layout includes Location, a Store for the access payload, and the page container
|
|
68
|
+
app.layout = html.Div([
|
|
69
|
+
dcc.Location(id='url', refresh=False),
|
|
70
|
+
dcc.Store(id='access-level-store', data=None, storage_type='memory'),
|
|
71
|
+
dash.page_container,
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
# Register the same clientside callback used in the single-page example to populate the store
|
|
75
|
+
app.clientside_callback(
|
|
76
|
+
ClientsideFunction(namespace='clientside', function_name='requestAccessLevel'),
|
|
77
|
+
Output('access-level-store', 'data'),
|
|
78
|
+
Input('url', 'pathname')
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
server = app.server
|
|
82
|
+
|
|
83
|
+
if __name__ == '__main__':
|
|
84
|
+
app.run_server(debug=True)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### pages/home.py
|
|
88
|
+
```python
|
|
89
|
+
from dash import html, register_page, Input, Output, dash
|
|
90
|
+
from usethatapp.webapps import get_product
|
|
91
|
+
|
|
92
|
+
register_page(
|
|
93
|
+
__name__,
|
|
94
|
+
name='Home',
|
|
95
|
+
path='/'
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Page layout is a placeholder container which we'll populate via a callback that
|
|
99
|
+
# listens to the shared `access-level-store` placed by the clientside JS.
|
|
100
|
+
def layout(**url_vars):
|
|
101
|
+
return html.Div(id='home-content')
|
|
102
|
+
|
|
103
|
+
# Use dash.callback (so the callback is registered on the global app when pages are loaded)
|
|
104
|
+
@dash.callback(Output('home-content', 'children'), Input('access-level-store', 'data'))
|
|
105
|
+
def display_content_based_on_access(data):
|
|
106
|
+
# Use the helper function to parse the message and determine the licensed product
|
|
107
|
+
try:
|
|
108
|
+
product = get_product(
|
|
109
|
+
data['message'],
|
|
110
|
+
public_key_path=r'/Path/To/File/Containing/UseThatApp_Public_Key',
|
|
111
|
+
private_key_path=r'/Path/To/File/Containing/My_UTA_Private_Key'
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if product == 'Pro':
|
|
115
|
+
return html.Div([html.H1(["Paid Content"])])
|
|
116
|
+
except Exception as e:
|
|
117
|
+
# If there's an error (e.g. clientside hasn't populated the store yet), fall back to free content
|
|
118
|
+
dash.get_app().server.logger.exception("Error determining product: %s", e)
|
|
119
|
+
return html.Div([html.H1(["Free Content"])])
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
# Change Log
|
|
123
|
+
|
|
124
|
+
## Version 0.1.0 (03/19/2026)
|
|
125
|
+
- Initial release
|
|
126
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=82.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "usethatapp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "For use in your UseThatApp Web Apps"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "UseThatApp", email = "support@usethatapp.com"}]
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files= ["LICENSE"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
]
|
|
17
|
+
keywords = ["UseThatApp", "Python", "Web Apps"]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"cryptography == 46.0.5",
|
|
20
|
+
]
|
|
21
|
+
requires-python = ">=3.8"
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/UseThatApp/python"
|
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from cryptography.exceptions import InvalidSignature
|
|
2
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
3
|
+
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def decrypt_message(private_key: rsa.RSAPrivateKey, encrypted_message: bytes) -> bytes:
|
|
7
|
+
"""Decrypts a message
|
|
8
|
+
|
|
9
|
+
Note: Uses SHA256 encryption
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
private_key: the private key used for decryption
|
|
13
|
+
encrypted_message: the message to be decrypted
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
The decrypted message
|
|
17
|
+
"""
|
|
18
|
+
decrypted_message = private_key.decrypt(
|
|
19
|
+
encrypted_message,
|
|
20
|
+
padding.OAEP(
|
|
21
|
+
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
|
22
|
+
algorithm=hashes.SHA256(),
|
|
23
|
+
label=None
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
return decrypted_message
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def verify_signature(public_key: rsa.RSAPublicKey, signature: bytes, message: bytes) -> bool:
|
|
30
|
+
"""Verifies a signature on an encrypted message
|
|
31
|
+
|
|
32
|
+
Note: Uses SHA256 encryption
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
public_key: the public key used for signature verification
|
|
36
|
+
signature: the signature to be verified
|
|
37
|
+
message: the message signed by the signature
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
True if verification passes, False otherwise
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
public_key.verify(
|
|
44
|
+
signature,
|
|
45
|
+
message,
|
|
46
|
+
padding.PSS(
|
|
47
|
+
mgf=padding.MGF1(hashes.SHA256()),
|
|
48
|
+
salt_length=padding.PSS.MAX_LENGTH
|
|
49
|
+
),
|
|
50
|
+
hashes.SHA256()
|
|
51
|
+
)
|
|
52
|
+
except InvalidSignature:
|
|
53
|
+
return False
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Keys:
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def read_public_key_from_string(cls, pem_str: str) -> rsa.RSAPublicKey:
|
|
61
|
+
pem_public_key = pem_str.encode().decode('unicode-escape').encode()
|
|
62
|
+
return serialization.load_pem_public_key(pem_public_key)
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def read_public_key_from_file(cls, file_path: str) -> rsa.RSAPublicKey:
|
|
66
|
+
with open(file_path, 'rb') as key_file:
|
|
67
|
+
return serialization.load_pem_public_key(key_file.read())
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def read_private_key_from_string(cls, pem_str: str) -> rsa.RSAPrivateKey:
|
|
71
|
+
pem_private_key = pem_str.encode().decode('unicode-escape').encode()
|
|
72
|
+
return serialization.load_pem_private_key(pem_private_key, password=None)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def read_private_key_from_file(cls, file_path: str) -> rsa.RSAPrivateKey:
|
|
76
|
+
with open(file_path, 'rb') as key_file:
|
|
77
|
+
return serialization.load_pem_private_key(key_file.read(), password=None)
|
|
78
|
+
|
|
79
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from .encryption import Keys, decrypt_message, verify_signature
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _normalize_hex(h: str) -> str:
|
|
8
|
+
if h is None:
|
|
9
|
+
raise ValueError("hex input is None")
|
|
10
|
+
h = h.strip()
|
|
11
|
+
if h.startswith("0x") or h.startswith("0X"):
|
|
12
|
+
h = h[2:]
|
|
13
|
+
if len(h) % 2 != 0:
|
|
14
|
+
# hex strings must have an even length
|
|
15
|
+
raise ValueError("invalid hex string (odd length)")
|
|
16
|
+
return h
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_product(
|
|
20
|
+
message,
|
|
21
|
+
public_key_path: str,
|
|
22
|
+
private_key_path: str,
|
|
23
|
+
encoding: str = "utf-8",
|
|
24
|
+
) -> Union[str, bytes]:
|
|
25
|
+
"""Verify a hex signature and decrypt a hex-encoded encrypted message from a JSON message.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
message: a dict or JSON string containing two keys: "signature" and "contents",
|
|
29
|
+
both values should be hex strings (optionally prefixed with 0x).
|
|
30
|
+
public_key_path: path to the PEM public key file used to verify the signature
|
|
31
|
+
private_key_path: path to the PEM private key file used to decrypt the message
|
|
32
|
+
encoding: if provided, the decrypted bytes will be decoded to a string using this encoding;
|
|
33
|
+
if decoding fails, the raw bytes are returned.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The decrypted message as a string (when decoding succeeds) or bytes.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ValueError: on invalid inputs, missing keys, or failed signature verification.
|
|
40
|
+
"""
|
|
41
|
+
# accept either a dict or a JSON string
|
|
42
|
+
if isinstance(message, str):
|
|
43
|
+
try:
|
|
44
|
+
message_obj = json.loads(message)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
raise ValueError(f"failed to parse message JSON: {e}")
|
|
47
|
+
elif isinstance(message, dict):
|
|
48
|
+
message_obj = message
|
|
49
|
+
else:
|
|
50
|
+
raise ValueError("message must be a dict or JSON string")
|
|
51
|
+
|
|
52
|
+
# extract expected fields
|
|
53
|
+
try:
|
|
54
|
+
signature_hex = message_obj["signature"]
|
|
55
|
+
encrypted_message_hex = message_obj.get("contents") or message_obj.get("content")
|
|
56
|
+
except Exception as e:
|
|
57
|
+
raise ValueError(f"message must contain 'signature' and 'contents' fields: {e}")
|
|
58
|
+
|
|
59
|
+
if encrypted_message_hex is None:
|
|
60
|
+
raise ValueError("message missing 'contents' field")
|
|
61
|
+
|
|
62
|
+
# normalize and convert hex inputs to bytes
|
|
63
|
+
try:
|
|
64
|
+
sig_hex = _normalize_hex(signature_hex)
|
|
65
|
+
msg_hex = _normalize_hex(encrypted_message_hex)
|
|
66
|
+
signature = bytes.fromhex(sig_hex)
|
|
67
|
+
encrypted_message = bytes.fromhex(msg_hex)
|
|
68
|
+
except ValueError as e:
|
|
69
|
+
raise ValueError(f"invalid hex input: {e}")
|
|
70
|
+
|
|
71
|
+
# load keys from files using the Keys helpers in encryption.py
|
|
72
|
+
try:
|
|
73
|
+
public_key = Keys.read_public_key_from_file(public_key_path)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
raise ValueError(f"failed to read public key from '{public_key_path}': {e}")
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
private_key = Keys.read_private_key_from_file(private_key_path)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise ValueError(f"failed to read private key from '{private_key_path}': {e}")
|
|
81
|
+
|
|
82
|
+
# verify the signature against the encrypted message
|
|
83
|
+
verified = verify_signature(public_key, signature, encrypted_message)
|
|
84
|
+
if not verified:
|
|
85
|
+
raise ValueError("signature verification failed")
|
|
86
|
+
|
|
87
|
+
# decrypt the message
|
|
88
|
+
decrypted = decrypt_message(private_key, encrypted_message)
|
|
89
|
+
|
|
90
|
+
# try to decode to text; if decoding fails, return raw bytes
|
|
91
|
+
try:
|
|
92
|
+
return decrypted.decode(encoding)
|
|
93
|
+
except Exception:
|
|
94
|
+
return decrypted
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = ["get_product"]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: usethatapp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: For use in your UseThatApp Web Apps
|
|
5
|
+
Author-email: UseThatApp <support@usethatapp.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/UseThatApp/python
|
|
8
|
+
Keywords: UseThatApp,Python,Web Apps
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: cryptography==46.0.5
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# Summary
|
|
18
|
+
Use this package with your UseThatApp WebApps to acquire user license info from UseThatApp.com.
|
|
19
|
+
|
|
20
|
+
# Usage
|
|
21
|
+
|
|
22
|
+
## Single Page Dash App
|
|
23
|
+
In a simple single page Dash-Plotly application.
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from dash import Dash, dcc, html, Input, Output, ClientsideFunction
|
|
27
|
+
from usethatapp.webapps import get_product
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
app = Dash(__name__, external_scripts=[
|
|
31
|
+
"https://cdn.jsdelivr.net/gh/UseThatApp/cdn@latest/usethatapp.js"
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
app.layout = html.Div([
|
|
35
|
+
dcc.Location(id='url', refresh=False),
|
|
36
|
+
html.Div(id='content'),
|
|
37
|
+
dcc.Store(id='access-level-store', data=None, storage_type='memory'),
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
app.clientside_callback(
|
|
41
|
+
ClientsideFunction(namespace='clientside', function_name='requestAccessLevel'),
|
|
42
|
+
Output("access-level-store", "data"),
|
|
43
|
+
Input('url', 'pathname')
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@app.callback(
|
|
47
|
+
Output('content', 'children'),
|
|
48
|
+
Input("access-level-store", "data")
|
|
49
|
+
)
|
|
50
|
+
def display_content_based_on_access(data):
|
|
51
|
+
try:
|
|
52
|
+
product = get_product(
|
|
53
|
+
data['message'],
|
|
54
|
+
public_key_path=r'/Path/To/File/Containing/UseThatApp_Public_Key',
|
|
55
|
+
private_key_path=r'/Path/To/File/Containing/My_UTA_Private_Key'
|
|
56
|
+
)
|
|
57
|
+
# display content based on licensed plan
|
|
58
|
+
if product == 'Pro':
|
|
59
|
+
return html.Div([html.H1(["Paid Content"])])
|
|
60
|
+
except Exception as e:
|
|
61
|
+
# If there's an error (e.g. clientside hasn't populated the store yet), fall back to free content
|
|
62
|
+
app.logger.exception("Error determining product: %s", e)
|
|
63
|
+
return html.Div([html.H1(["Free Content"])])
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Mult-Page Dash App
|
|
67
|
+
In a multi-page Dash-Plotly application (using Dash Pages). The pattern below mirrors the single-page example: the main `app` registers a clientside callback that places the UseThatApp message into a `dcc.Store`, and each page reads that store (via a callback) and calls `get_product` to decide what to render.
|
|
68
|
+
|
|
69
|
+
### app.py
|
|
70
|
+
```python
|
|
71
|
+
import dash
|
|
72
|
+
from dash import Dash, html, dcc, Input, Output, ClientsideFunction
|
|
73
|
+
|
|
74
|
+
APP_TITLE = "Dash App"
|
|
75
|
+
|
|
76
|
+
app = Dash(
|
|
77
|
+
__name__,
|
|
78
|
+
title=APP_TITLE,
|
|
79
|
+
use_pages=True, # Allows us to register pages
|
|
80
|
+
external_scripts=["https://cdn.jsdelivr.net/gh/UseThatApp/cdn@latest/usethatapp.js"],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Top-level layout includes Location, a Store for the access payload, and the page container
|
|
84
|
+
app.layout = html.Div([
|
|
85
|
+
dcc.Location(id='url', refresh=False),
|
|
86
|
+
dcc.Store(id='access-level-store', data=None, storage_type='memory'),
|
|
87
|
+
dash.page_container,
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
# Register the same clientside callback used in the single-page example to populate the store
|
|
91
|
+
app.clientside_callback(
|
|
92
|
+
ClientsideFunction(namespace='clientside', function_name='requestAccessLevel'),
|
|
93
|
+
Output('access-level-store', 'data'),
|
|
94
|
+
Input('url', 'pathname')
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
server = app.server
|
|
98
|
+
|
|
99
|
+
if __name__ == '__main__':
|
|
100
|
+
app.run_server(debug=True)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### pages/home.py
|
|
104
|
+
```python
|
|
105
|
+
from dash import html, register_page, Input, Output, dash
|
|
106
|
+
from usethatapp.webapps import get_product
|
|
107
|
+
|
|
108
|
+
register_page(
|
|
109
|
+
__name__,
|
|
110
|
+
name='Home',
|
|
111
|
+
path='/'
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Page layout is a placeholder container which we'll populate via a callback that
|
|
115
|
+
# listens to the shared `access-level-store` placed by the clientside JS.
|
|
116
|
+
def layout(**url_vars):
|
|
117
|
+
return html.Div(id='home-content')
|
|
118
|
+
|
|
119
|
+
# Use dash.callback (so the callback is registered on the global app when pages are loaded)
|
|
120
|
+
@dash.callback(Output('home-content', 'children'), Input('access-level-store', 'data'))
|
|
121
|
+
def display_content_based_on_access(data):
|
|
122
|
+
# Use the helper function to parse the message and determine the licensed product
|
|
123
|
+
try:
|
|
124
|
+
product = get_product(
|
|
125
|
+
data['message'],
|
|
126
|
+
public_key_path=r'/Path/To/File/Containing/UseThatApp_Public_Key',
|
|
127
|
+
private_key_path=r'/Path/To/File/Containing/My_UTA_Private_Key'
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if product == 'Pro':
|
|
131
|
+
return html.Div([html.H1(["Paid Content"])])
|
|
132
|
+
except Exception as e:
|
|
133
|
+
# If there's an error (e.g. clientside hasn't populated the store yet), fall back to free content
|
|
134
|
+
dash.get_app().server.logger.exception("Error determining product: %s", e)
|
|
135
|
+
return html.Div([html.H1(["Free Content"])])
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
# Change Log
|
|
139
|
+
|
|
140
|
+
## Version 0.1.0 (03/19/2026)
|
|
141
|
+
- Initial release
|
|
142
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
usethatapp/__init__.py
|
|
5
|
+
usethatapp/encryption.py
|
|
6
|
+
usethatapp/webapps.py
|
|
7
|
+
usethatapp.egg-info/PKG-INFO
|
|
8
|
+
usethatapp.egg-info/SOURCES.txt
|
|
9
|
+
usethatapp.egg-info/dependency_links.txt
|
|
10
|
+
usethatapp.egg-info/requires.txt
|
|
11
|
+
usethatapp.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cryptography==46.0.5
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
usethatapp
|