Skip to content

Commit 8051f58

Browse files
committed
login
1 parent aaec014 commit 8051f58

9 files changed

Lines changed: 727 additions & 96 deletions

File tree

datalayer_core/authn/server/http_server.py

Lines changed: 60 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,20 @@
4646
logger = logging.getLogger(__name__)
4747

4848

49+
def _normalize_navigation_target(candidate: str | None) -> str | None:
50+
if candidate is None:
51+
return None
52+
value = str(candidate).strip()
53+
if not value:
54+
return None
55+
parsed = urllib.parse.urlparse(value)
56+
if parsed.scheme or parsed.netloc:
57+
return None
58+
if not value.startswith("/") or value.startswith("//"):
59+
return None
60+
return value
61+
62+
4963
class LoginRequestHandler(SimpleHTTPRequestHandler):
5064
"""
5165
Handle HTTP requests for authentication flow.
@@ -83,22 +97,46 @@ def _save_token(self, query: str) -> None:
8397

8498
user_raw = arguments.get("user", [""])[0]
8599
token = arguments.get("token", [""])[0]
100+
navigation_candidate = (
101+
arguments.get("navigate_to", [""])[0]
102+
or arguments.get("navigation", [""])[0]
103+
or arguments.get("post_auth_redirect", [""])[0]
104+
or arguments.get("redirect_url", [""])[0]
105+
)
106+
navigation_target = _normalize_navigation_target(navigation_candidate)
86107

87108
if not user_raw or not token:
88109
self.send_error(HTTPStatus.BAD_REQUEST, "User and token must be provided.")
89110

90111
user = json.loads(urllib.parse.unquote(user_raw))
91-
content = AUTH_SUCCESS_PAGE.format(
92-
user_key=DATALAYER_IAM_USER_KEY,
93-
uid=user.get("uid"),
94-
handle=user["handle_s"],
95-
first_name=user["first_name_t"],
96-
last_name=user["last_name_t"],
97-
email=user["email_s"],
98-
display_name=" ".join((user["first_name_t"], user["last_name_t"])).strip(),
99-
token_key=DATALAYER_IAM_TOKEN_KEY,
100-
token=token,
101-
base_url="/",
112+
content = (
113+
AUTH_SUCCESS_PAGE
114+
.replace("__USER_KEY__", DATALAYER_IAM_USER_KEY)
115+
.replace("__TOKEN_KEY__", DATALAYER_IAM_TOKEN_KEY)
116+
.replace("__UID_JSON__", json.dumps(user.get("uid", "")))
117+
.replace("__HANDLE_JSON__", json.dumps(user.get("handle_s", "")))
118+
.replace(
119+
"__FIRST_NAME_JSON__", json.dumps(user.get("first_name_t", ""))
120+
)
121+
.replace(
122+
"__LAST_NAME_JSON__", json.dumps(user.get("last_name_t", ""))
123+
)
124+
.replace("__EMAIL_JSON__", json.dumps(user.get("email_s", "")))
125+
.replace(
126+
"__DISPLAY_NAME_JSON__",
127+
json.dumps(
128+
" ".join(
129+
(
130+
user.get("first_name_t", ""),
131+
user.get("last_name_t", ""),
132+
)
133+
).strip()
134+
),
135+
)
136+
.replace("__TOKEN_JSON__", json.dumps(token))
137+
.replace(
138+
"__NAVIGATION_TARGET_JSON__", json.dumps(navigation_target or "")
139+
)
102140
).encode("UTF-8", "replace")
103141
self.send_response(HTTPStatus.OK)
104142
self.send_header("Content-type", "text/html")
@@ -112,14 +150,15 @@ def do_GET(self) -> None:
112150
if path.strip("/").endswith("callback"):
113151
self._save_token(query)
114152
elif path in {"/", "/datalayer/login/cli"}:
115-
content = LANDING_PAGE.format(
116-
config=json.dumps(
117-
{
118-
"runUrl": self.server.run_url, # type: ignore
119-
"iamRunUrl": self.server.iam_url, # type: ignore
120-
"whiteLabel": False,
121-
}
122-
)
153+
config_json = json.dumps(
154+
{
155+
"runUrl": self.server.run_url, # type: ignore
156+
"iamRunUrl": self.server.iam_url, # type: ignore
157+
"whiteLabel": False,
158+
}
159+
)
160+
content = LANDING_PAGE.replace(
161+
"__DATALAYER_CONFIG_JSON__", config_json
123162
).encode("UTF-8", "replace")
124163
self.send_response(HTTPStatus.OK)
125164
self.send_header("Content-type", "text/html")
@@ -203,16 +242,6 @@ def __init__(
203242
bind_and_activate : bool, default True
204243
Whether to bind and activate the server.
205244
"""
206-
try:
207-
import datalayer_ui # noqa: F401
208-
except Exception:
209-
print("Sorry, I can not show the login page...")
210-
print(
211-
"Check the datalayer_ui python package is available in your environment"
212-
)
213-
import sys
214-
215-
sys.exit(-1)
216245
# Use DatalayerURLs for proper URL configuration
217246
self._urls = DatalayerURLs.from_environment(run_url=run_url)
218247
self.run_url = self._urls.run_url
@@ -246,14 +275,12 @@ def finish_request(self, request: t.Any, client_address: str) -> None:
246275
client_address : str
247276
The client address.
248277
"""
249-
import datalayer_ui
250-
251-
DATALAYER_UI_PATH = Path(datalayer_ui.__file__).parent
278+
datalayer_core_static_path = HERE.parent.parent / "static"
252279
self.RequestHandlerClass(
253280
request,
254281
client_address,
255282
self, # type: ignore[arg-type]
256-
directory=str(DATALAYER_UI_PATH / "static"), # type: ignore[call-arg]
283+
directory=str(datalayer_core_static_path), # type: ignore[call-arg]
257284
)
258285

259286

datalayer_core/authn/server/pages.py

Lines changed: 99 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,39 +11,120 @@
1111
<meta charset="UTF-8"/>
1212
<title>🪐 ⚪ Datalayer Login</title>
1313
<script id="datalayer-config-data" type="application/json">
14-
{config}
14+
__DATALAYER_CONFIG_JSON__
1515
</script>
1616
<link rel="shortcut icon" href="data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAC7SURBVFiF7ZU9CgIxEIXfTHbPopfYc+pJ9AALtmJnZSOIoJWFoCTzLHazxh/Ebpt5EPIxM8XXTCKTxYyMCYwJFhOYCo4JFiMuu317PZwaqEBUIar4YMmskL73DytGjgu4gAt4PDJdzkkzMBloBhqBgcu69XW+1I+rNSQESNDuaMEhdP/Fj/7oW+ACLuACHk/3F5BAfuMLBjm8/ZnxNvNtHmY4b7Ztut0bqStoVSHfWj9Z6mr8LXABF3CBB3nvkDfEVN6PAAAAAElFTkSuQmCC" type="image/x-icon" />
17-
<script defer src="/cli.datalayer-ui.js"></script>
17+
<link rel="stylesheet" href="/core.css" />
18+
<script>
19+
globalThis.process = globalThis.process || { env: { NODE_ENV: 'production' } };
20+
</script>
21+
<script defer src="/cli.datalayer-core.js"></script>
1822
</head>
1923
<body>
24+
<div id="root"></div>
2025
</body>
2126
</html>"""
2227

2328

2429
AUTH_SUCCESS_PAGE = """<!DOCTYPE html>
2530
<html>
2631
<head>
32+
<meta charset="utf-8" />
33+
<title>Datalayer CLI Login</title>
34+
<style>
35+
body {
36+
margin: 0;
37+
min-height: 100vh;
38+
display: flex;
39+
align-items: center;
40+
justify-content: center;
41+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
42+
background: #f6f8fa;
43+
color: #1f2328;
44+
}
45+
.card {
46+
width: min(560px, calc(100vw - 32px));
47+
border: 1px solid #d0d7de;
48+
border-radius: 12px;
49+
background: #ffffff;
50+
box-shadow: 0 8px 24px rgba(140, 149, 159, 0.2);
51+
padding: 24px;
52+
}
53+
h2 {
54+
margin: 0 0 8px;
55+
font-size: 20px;
56+
}
57+
p {
58+
margin: 0;
59+
color: #57606a;
60+
}
61+
.muted {
62+
margin-top: 12px;
63+
font-size: 13px;
64+
}
65+
</style>
2766
<script type="module">
28-
// Store the user information
29-
window.localStorage.setItem(
30-
'{user_key}',
31-
JSON.stringify({{
32-
uid: '{uid}',
33-
handle: '{handle}',
34-
firstName: '{first_name}',
35-
lastName: '{last_name}',
36-
email: '{email}',
37-
displayName: '{display_name}'
38-
}})
39-
);
40-
// Store the token.
41-
localStorage.setItem('{token_key}', '{token}');
42-
// Redirect to login page.
43-
window.location.pathname = '/datalayer/login/cli';
67+
const user = {
68+
uid: __UID_JSON__,
69+
handle: __HANDLE_JSON__,
70+
firstName: __FIRST_NAME_JSON__,
71+
lastName: __LAST_NAME_JSON__,
72+
email: __EMAIL_JSON__,
73+
displayName: __DISPLAY_NAME_JSON__,
74+
};
75+
const token = __TOKEN_JSON__;
76+
const userHandle = __HANDLE_JSON__ || '';
77+
const navigationTarget = __NAVIGATION_TARGET_JSON__;
78+
79+
window.localStorage.setItem('__USER_KEY__', JSON.stringify(user));
80+
window.localStorage.setItem('__TOKEN_KEY__', token);
81+
82+
const statusNode = document.getElementById('status');
83+
const detailsNode = document.getElementById('details');
84+
const whoAmINode = document.getElementById('whoami');
85+
86+
if (whoAmINode) {
87+
whoAmINode.textContent = `Authenticated as ${userHandle || 'unknown'}.`;
88+
}
89+
90+
const setStatus = (title, details) => {
91+
if (statusNode) statusNode.textContent = title;
92+
if (detailsNode) detailsNode.textContent = details;
93+
};
94+
95+
const finalize = async () => {
96+
await fetch('/', {
97+
method: 'POST',
98+
headers: { 'Content-Type': 'application/json' },
99+
body: JSON.stringify({
100+
user_handle: userHandle,
101+
token,
102+
}),
103+
});
104+
};
105+
106+
if (navigationTarget) {
107+
finalize()
108+
.then(() => {
109+
setStatus('Authentication successful', 'Redirecting to your requested page...');
110+
window.location.replace(navigationTarget);
111+
})
112+
.catch((error) => {
113+
console.error('Failed to finalize CLI authentication.', error);
114+
setStatus('Authentication succeeded, but CLI finalization failed', 'You can retry login from your terminal.');
115+
});
116+
} else {
117+
setStatus('Authentication successful', 'Redirecting to the CLI confirmation page...');
118+
window.location.replace('/');
119+
}
44120
</script>
45121
</head>
46122
<body>
123+
<main class="card">
124+
<h2 id="status">Finalizing authentication...</h2>
125+
<p id="details">Please wait while we complete your CLI sign-in.</p>
126+
<p class="muted" id="whoami"></p>
127+
</main>
47128
</body>
48129
</html>"""
49130

datalayer_core/cli/commands/authn.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,11 @@ def login(
220220
console.print("[yellow]CLI-only authentication selected[/yellow]")
221221
credentials = _ask_credentials()
222222

223-
if credentials.get("credentials_type") == "token":
223+
if credentials.get("credentials_type") == "api_key":
224224
asyncio.run(
225-
_login_with_token(auth, credentials["token"], urls.run_url)
225+
_login_with_api_key(
226+
auth, credentials["api_key"], urls.run_url
227+
)
226228
)
227229
else:
228230
asyncio.run(
@@ -236,7 +238,7 @@ def login(
236238
else:
237239
# Browser-based OAuth
238240
console.print(
239-
"[yellow]No token found. Starting browser-based authentication...[/yellow]"
241+
"[yellow]No API key found. Starting browser-based authentication...[/yellow]"
240242
)
241243
_authenticate_with_browser(auth, urls.run_url)
242244

@@ -247,22 +249,22 @@ def login(
247249
raise typer.Exit(1)
248250

249251

250-
async def _login_with_token(
251-
auth: AuthenticationManager, token: str, server_url: str
252+
async def _login_with_api_key(
253+
auth: AuthenticationManager, api_key: str, server_url: str
252254
) -> None:
253-
"""Login using token via Client."""
255+
"""Login using API key via Client."""
254256
try:
255-
user, _ = await auth.login(token=token)
257+
user, _ = await auth.login(token=api_key)
256258
user_handle = user.get("handle_s", user.get("handle", "unknown"))
257259

258260
console.print(
259261
f"🎉 Successfully authenticated as [cyan]{user_handle}[/cyan] on [green]{server_url}[/green]"
260262
)
261-
console.print("✅ Token saved for future use")
263+
console.print("🔑 API key saved for future use")
262264

263265
except Exception as e:
264266
console.print(f"[red]Authentication failed: {e}[/red]")
265-
console.print("[yellow]Please check your token and try again.[/yellow]")
267+
console.print("[yellow]Please check your API key and try again.[/yellow]")
266268
raise typer.Exit(1)
267269

268270

@@ -277,7 +279,7 @@ async def _login_with_credentials(
277279
console.print(
278280
f"🎉 Successfully authenticated as [cyan]{user_handle}[/cyan] on [green]{server_url}[/green]"
279281
)
280-
console.print("✅ Token saved for future use")
282+
console.print("🔑 API key saved for future use")
281283

282284
except Exception as e:
283285
console.print(f"[red]Failed to authenticate as {handle} on {server_url}[/red]")
@@ -294,7 +296,7 @@ def _ask_credentials() -> dict[str, str]:
294296
"message": "How do you want to log in?",
295297
"choices": [
296298
{"name": "Username/Password", "value": "password"},
297-
{"name": "Token", "value": "token"},
299+
{"name": "API key", "value": "api_key"},
298300
],
299301
},
300302
{
@@ -315,11 +317,11 @@ def _ask_credentials() -> dict[str, str]:
315317
},
316318
{
317319
"type": "password",
318-
"name": "token",
319-
"message": "Token:",
320-
"when": lambda x: x["credentials_type"] == "token",
320+
"name": "api_key",
321+
"message": "API key:",
322+
"when": lambda x: x["credentials_type"] == "api_key",
321323
"validate": lambda x: (
322-
True if len(x) >= 8 else "Token must have at least 8 characters"
324+
True if len(x) >= 8 else "API key must have at least 8 characters"
323325
),
324326
},
325327
]
@@ -389,7 +391,7 @@ def _authenticate_with_browser(auth: AuthenticationManager, server_url: str) ->
389391
console.print(
390392
f"🎉 Successfully authenticated as [cyan]{user_handle}[/cyan] on [green]{server_url}[/green]"
391393
)
392-
console.print("✅ Token saved for future use")
394+
console.print("🔑 API key saved for future use")
393395

394396
except KeyboardInterrupt:
395397
console.print("\n[yellow]Authentication cancelled by user[/yellow]")
@@ -420,7 +422,7 @@ def logout(
420422
asyncio.run(auth.logout())
421423

422424
console.print(f"👋 Logged out from [green]{urls.run_url}[/green]")
423-
console.print("✅ Stored token cleared")
425+
console.print("✅ Stored API key cleared")
424426

425427
except Exception as e:
426428
console.print(f"[red]Logout failed: {e}[/red]")

datalayer_core/templates/index.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,13 @@
3737
href="data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAC7SURBVFiF7ZU9CgIxEIXfTHbPopfYc+pJ9AALtmJnZSOIoJWFoCTzLHazxh/Ebpt5EPIxM8XXTCKTxYyMCYwJFhOYCo4JFiMuu317PZwaqEBUIar4YMmskL73DytGjgu4gAt4PDJdzkkzMBloBhqBgcu69XW+1I+rNSQESNDuaMEhdP/Fj/7oW+ACLuACHk/3F5BAfuMLBjm8/ZnxNvNtHmY4b7Ztut0bqStoVSHfWj9Z6mr8LXABF3CBB3nvkDfEVN6PAAAAAElFTkSuQmCC"
3838
type="image/x-icon"
3939
/>
40+
<link rel="stylesheet" href="{{ base_url }}static/datalayer_core/core.css" />
41+
<script>
42+
globalThis.process = globalThis.process || { env: { NODE_ENV: 'production' } };
43+
</script>
4044
<script
4145
defer
42-
src="{{ base_url }}static/datalayer_ui/cli.datalayer-ui.js"
46+
src="{{ base_url }}static/datalayer_core/cli.datalayer-core.js"
4347
></script>
4448
</head>
4549
<body>

0 commit comments

Comments
 (0)