Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/facade/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ These accept JSON strings for complex configuration:
| Requirement | Default | Description |
|---|---|---|
| `login_path` | `/login.html` | Path to redirect unauthenticated users (no token in localStorage) |
| `login_redirect_param` | `""` _(off)_ | Query param name appended to `login_path` carrying the user's current relative URL, so the login flow can return them after auth. Empty disables. See [Post-login redirect](#post-login-redirect). |

### Theming

Expand Down Expand Up @@ -204,6 +205,23 @@ Only override what differs from defaults.
value: "custom:logo"
```

### Post-login redirect

Off by default. When enabled, the facade appends the current page's relative URL to `login_path` so the login flow can return the user to where they were after authenticating.

```yaml
- name: login_redirect_param
value: "redirect_to"
```

A user opening a deep link like `/c/abc-123` without a valid token will be sent to:

```
/login.html?redirect_to=%2Fc%2Fabc-123
```

The login page reads `redirect_to` from the query string and navigates the user there after successful auth.

### Extra scripts

Inject external `<script>` tags into the facade `index.html` before the Web Host bundle loads. Useful for host-context integrations (analytics, third-party bridges) that need to run in the top-level window, not inside child iframes.
Expand Down Expand Up @@ -236,6 +254,7 @@ Scripts are fetched in parallel and awaited before the Web Host bundle is import
"iframe_origin": "https://web-host.wippy.ai",
"iframe_url": "https://web-host.wippy.ai/webcomponents-1.0.26/iframe.html?waitForCustomConfig",
"login_path": "/login.html",
"login_redirect_param": null,
"env": {
"APP_API_URL": "http://localhost:8085",
"APP_AUTH_API_URL": "http://localhost:8085",
Expand Down
13 changes: 13 additions & 0 deletions src/facade/_index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,19 @@ entries:
path: .default
default: /login.html

- name: login_redirect_param
kind: ns.requirement
meta:
description: |
Query param name appended to login_path with the current page's
relative URL (path + search + hash), so the login flow can return the
user to the originally requested page after authentication. Empty
disables. Common values: "redirect_to", "next", "return_to".
targets:
- entry: wippy.facade:login_redirect_param
path: .default
default: ""

# Theming — global scope (applied to both host and children)
- name: custom_css
kind: ns.requirement
Expand Down
1 change: 1 addition & 0 deletions src/facade/config_handler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ local function handler()
iframe_origin = iframe_origin,
iframe_url = iframe_url,
login_path = get_req("login_path"),
login_redirect_param = non_empty_or_nil(get_req("login_redirect_param")),
mode = fe_mode,
module_file = module_file,

Expand Down
32 changes: 32 additions & 0 deletions src/facade/config_handler_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ local REQ_NAMES: {string} = {
"host_custom_css", "host_css_variables", "host_icon_sets",
"children_custom_css", "children_css_variables",
"app_title", "app_icon", "app_name", "login_path",
"login_redirect_param",
"api_routes", "additional_nav_items", "state_cache",
"allow_additional_tags", "chat", "axios_defaults",
"extra_scripts",
Expand Down Expand Up @@ -41,6 +42,7 @@ local function setup_registry(overrides: {[string]: string}?)
app_icon = "wippy:logo",
app_name = "Wippy AI",
login_path = "/login.html",
login_redirect_param = "",
api_routes = "{}",
additional_nav_items = "[]",
state_cache = "{}",
Expand Down Expand Up @@ -242,6 +244,36 @@ local function define_tests()
end)
end)

test.describe("login redirect param", function()
test.before_each(function()
setup_registry()
end)

test.after_each(function()
teardown_registry()
end)

test.it("defaults to empty (feature off)", function()
local entry = registry.get(NS .. "login_redirect_param")
test.not_nil(entry)
test.eq(entry.data.default, "")
end)

test.it("can be set to a custom param name", function()
local snap = registry.snapshot()
local changes = snap:changes()
changes:update({
id = NS .. "login_redirect_param",
kind = "ns.requirement",
data = { default = "redirect_to" },
})
changes:apply()

local entry = registry.get(NS .. "login_redirect_param")
test.eq(entry.data.default, "redirect_to")
end)
end)

test.describe("config JSON structure (wippy-context-2.0)", function()
test.it("builds complete config object", function()
local config = {
Expand Down
32 changes: 26 additions & 6 deletions src/facade/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,34 @@
}
var cfg = await res.json();

function goToLogin() {
var loginPath = cfg.login_path || '/login.html';
var paramName = cfg.login_redirect_param;
if (!paramName) {
window.location.href = loginPath;
return;
}
try {
var loginUrl = new URL(loginPath, window.location.origin);
// Don't recurse: if we are already on the login page, skip the param.
if (loginUrl.pathname === window.location.pathname) {
window.location.href = loginPath;
return;
}
var rel = window.location.pathname + window.location.search + window.location.hash;
loginUrl.searchParams.set(paramName, rel);
window.location.href = loginUrl.toString();
} catch (e) {
window.location.href = loginPath;
}
}

var stored = localStorage.getItem(STORAGE_KEY);
if (!stored) { window.location.href = cfg.login_path || '/login.html'; throw new Error('no token'); }
if (!stored) { goToLogin(); throw new Error('no token'); }

var token;
try { token = JSON.parse(stored).token; }
catch (e) { window.location.href = cfg.login_path || '/login.html'; throw new Error('bad token'); }
catch (e) { goToLogin(); throw new Error('bad token'); }

if (Array.isArray(cfg.extraScripts) && cfg.extraScripts.length) {
await Promise.all(cfg.extraScripts.map(function (entry) {
Expand Down Expand Up @@ -133,8 +155,6 @@
context: { resourceId: '', resourceType: 'page' },
}, '#app');

var loginPath = cfg.login_path || '/login.html';

// Hide overlay when initial route resolves and page starts rendering
events.on('load', hideOverlay);

Expand Down Expand Up @@ -244,13 +264,13 @@

events.on('authExpired', function () {
localStorage.removeItem(STORAGE_KEY);
window.location.href = loginPath;
goToLogin();
});

events.on('error', function (err) {
console.error('Wippy critical error:', err);
localStorage.removeItem(STORAGE_KEY);
window.location.href = loginPath;
goToLogin();
});
} catch (err) {
console.error('Facade initialization failed:', err);
Expand Down
Loading