Skip to content
Open
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
25 changes: 25 additions & 0 deletions .github/workflows/v0-behavior.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: V0 behavior checks
on:
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
behavior:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '22'
cache: npm
- name: Install locked dependencies without lifecycle hooks
run: npm ci --ignore-scripts --legacy-peer-deps --no-audit --no-fund
- name: Run focused behavior and HTTP routing tests
run: npm run test:v0
- name: Generate local environment modules without credentials
env:
CI: 'false'
run: node tools/set-env
- name: Check application types and templates
run: node_modules/.bin/ngc --noEmit -p apps/36-blocks/tsconfig.app.json
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Working on proxy-ui

Owns the Angular public website, operator dashboard and embeddable authentication widget. Backend APIs, persistence and session policy belong to sibling `user-proxy`; connected applications own business authorization.

Start with the canonical [work state](../user-proxy/docs/WORK-STATE.md), [architecture](../user-proxy/docs/ARCHITECTURE.md), [capabilities](../user-proxy/docs/CAPABILITIES.md), [development](../user-proxy/docs/DEVELOPMENT.md), [security](../user-proxy/docs/SECURITY-OPERATIONS.md) and [design](../user-proxy/docs/DESIGN.md). Preserve existing README contribution rules and DESIGNGUIDE.md. Read widget integration/event guides before changing the custom element contract.

- Inspect status/diff; preserve unrelated changes. New application code uses TypeScript and existing standalone Angular patterns. Use Angular CLI scaffolding when practical; never change architecture solely to satisfy scaffolding.
- Install from lock using `npm ci --ignore-scripts --legacy-peer-deps`; generate ignored env modules with `node tools/set-env`. Review environment base URLs before starting: local config defaults to a remote test API.
- Run `node --test tools/tests/*.test.cjs`, affected builds and `git diff --check`. Existing Nx/Jest targets have known gaps: report exact commands and limitations in canonical work state.
- Keep error, denied, empty and loading states distinct; provide retry. Never render request data as HTML or log/store tokens. Client visibility does not replace server authorization.
- Update the single backend-owned capability matrix and work state alongside changes. Product/architecture guidance is linked, not duplicated here. Public claims require evidence; do not invent SLAs, guarantees or future functional controls.
16 changes: 2 additions & 14 deletions DESIGNGUIDE.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,8 @@
# Proxy UI Design Guide

## Setup Guide
## Setup

1. Clone https://github.com/Walkover-Web-Solution/proxy-UI.git.

2. This project requires Node version 14+, install Node from [here](https://nodejs.org/en/).

3. Go inside the cloned directory `proxy-UI` in any terminal and run these following commands in the terminal to install dependency and start local serve.

4. Run `npm i` for installation of dependencies.

5. Serving the project locally:
* For proxy: Run `npm start` to local serve the project.
* For Admin Panel: Run `npm run start:admin` to local serve the project.
* For chat-widget: Run `npm run start:chat-widget` to local serve the project.
* For otp-provider: Run `npm run start:otp-provider` to local serve the project.
Use the current [README](README.md) and canonical [development guide](../user-proxy/docs/DEVELOPMENT.md) for runtime, install and startup commands. Legacy admin/chat/OTP application commands formerly listed here no longer describe the supplied workspace. The following styling guidance remains reference material; use the canonical [design standards](../user-proxy/docs/DESIGN.md) for product behavior.

## Prerequisites
It is important to have a thorough understanding of **HTML**, **CSS** and its pre-processor **SASS** to contribute to this project. If unaware of any of the above then go through their documention first.
Expand Down
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@

1. Clone https://github.com/Walkover-Web-Solution/proxy-ui.git.

2. This project requires Node version 14+, install Node from [here](https://nodejs.org/en/).
2. This is Angular 21.2 / Nx 21.6 / TypeScript 5.9. Use a compatible Node runtime; see the [canonical development guide](../user-proxy/docs/DEVELOPMENT.md). Node 14 is unsupported.

3. Go inside the cloned directory `proxy-ui` in any terminal and run these following commands in the terminal to install dependency and start local serve.

4. Run `npm i` for installation of dependencies.
4. Run `npm ci --ignore-scripts --legacy-peer-deps` to install the locked dependencies. Review the development guide before enabling install hooks.

5. Run `npm start` to local serve the project.
5. Configure approved local environment values, run `node tools/set-env`, then `npm start`. The checked-in local API URL points to a remote test service; change it before exercising mutations.

Read [AGENTS.md](AGENTS.md), [current work state](../user-proxy/docs/WORK-STATE.md), [architecture](../user-proxy/docs/ARCHITECTURE.md) and the authoritative [capability matrix](../user-proxy/docs/CAPABILITIES.md). The API is the sibling `user-proxy` repository. `node --test tools/tests/*.test.cjs` runs focused behavior tests; legacy Nx/Jest targets have known gaps recorded in the work state.

The existing contribution and style guidance below remains applicable. Use the canonical development and design guides where obsolete runtime/module instructions conflict with the current standalone Angular architecture.

## Contributing Guide

Expand Down Expand Up @@ -344,3 +348,9 @@ We have a server [Marlin](marlin.phone91.com) [for Prod] & [sailfish](sailfish.p

If client origin is widget (origin = `chat`) [will implement for all in v2], we have an option to start meet from `client-card-component`.
For selected client we use `call-session-creds` API to fetch `room` & `jwt` to init socket connection and Jitsi meet room with Jitsi Options. [Jitsi Reference Doc](https://developer.8x8.com/jaas/docs/iframe-api-integration)

## Production integration — 29 October 2026

The V0 changes are reconciled with production commit `2b1deb17`. Production deliberately uses a browser-only build and removed `ProjectGuard`; this merge preserves both decisions and the newer production navigation, OAuth/client-secret features, and compact website spacing. The original feature branch's SSR and project-guard changes/tests are therefore not part of this production integration.

Validation: 23 applicable `npm run test:v0` checks, Angular type/template checking, and `npm run build:prod` passed, including the widget bundle and post-build packaging. Build warnings remain; this does not certify configured Firebase/provider login. Activity and Login as require the companion backend API/migrations from [user-proxy PR #1476](https://github.com/Walkover-Web-Solution/user-proxy/pull/1476) before live use. See the canonical [work state](../user-proxy/docs/WORK-STATE.md) for remaining integration gates.
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export class RootEffects {
switchMap((req) => {
return this.rootService.getProjects().pipe(
map((res) => {
if (res.hasError) {
if (res.hasError || !Array.isArray(res.data?.data)) {
this.showError(res.errors);
return rootActions.getAllProjectsError();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface IRootState {
//All project
allProjects: IPaginatedResponse<IProjects[]>;
projectInProcess: boolean;
projectLoadFailed: boolean;
}

export const initialState: IRootState = {
Expand All @@ -33,6 +34,7 @@ export const initialState: IRootState = {
clientsInProcess: false,
switchClientSuccess: false,
projectInProcess: false,
projectLoadFailed: false,
allProjects: null,
};

Expand Down Expand Up @@ -121,20 +123,23 @@ const _rootReducer = createReducer(
return {
...state,
projectInProcess: true,
projectLoadFailed: false,
};
}),
on(rootActions.getAllProjectSuccess, (state, { response }) => {
return {
...state,
allProjects: response,
projectInProcess: false,
projectLoadFailed: false,
};
}),
on(rootActions.getAllProjectsError, (state) => {
return {
...state,
allProjects: null,
projectInProcess: false,
projectLoadFailed: true,
};
})
);
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,10 @@ export const selectProjectInProcess = createSelector(
selectRootState,
(rootState: IRootState) => rootState.projectInProcess
);

/** One selector keeps project data and its request outcome consistent for guards. */
export const selectProjectAccessState = createSelector(selectRootState, (state: IRootState) => ({
projects: state.allProjects,
loading: state.projectInProcess,
failed: state.projectLoadFailed,
}));
225 changes: 225 additions & 0 deletions apps/36-blocks/src/app/panel/activity/activity.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
<section class="activity-page" aria-labelledby="activity-title">
<header>
<h1 id="activity-title">Activity</h1>
<p>
Investigate operator actions for the current client. Login as records include the actor, target, reason and
outcome.
</p>
<p>
Older operations may have no audit details. A started attempt without a final outcome needs investigation.
</p>
</header>

<details class="login-as">
<summary>Login as an end user</summary>
<p>
For client Owners, Admins and Super admins. This creates a session with the target user's access to their
application. The action and your reason are recorded in Activity. Use internal numeric IDs from the current
client. If no company is specified, the user's first company is checked for eligibility.
</p>
<p>
Confirm the user and company before continuing. Configure an HTTPS application redirect on the block
before starting; this panel does not display raw session tokens. The application remains responsible for
business permissions.
</p>
<form
class="activity-filters"
#loginForm="ngForm"
(ngSubmit)="loginForm.valid && loginAs()"
aria-label="Login as an end user"
>
<mat-form-field
><mat-label>Target user ID</mat-label
><input
matInput
name="loginTarget"
[(ngModel)]="loginTarget"
(ngModelChange)="clearLoginResult()"
[disabled]="loggingIn()"
required
inputmode="numeric"
pattern="[0-9]+"
/></mat-form-field>
<mat-form-field
><mat-label>Company ID (optional)</mat-label
><input
matInput
name="loginCompany"
[(ngModel)]="loginCompany"
(ngModelChange)="clearLoginResult()"
[disabled]="loggingIn()"
inputmode="numeric"
pattern="[0-9]+"
/></mat-form-field>
<mat-form-field class="reason-field"
><mat-label>Reason</mat-label
><textarea
matInput
name="loginReason"
[(ngModel)]="loginReason"
(ngModelChange)="clearLoginResult()"
[disabled]="loggingIn()"
required
maxlength="500"
rows="2"
placeholder="Support ticket or investigation purpose; do not include credentials"
></textarea>
</mat-form-field>
<label class="confirmation"
><input
type="checkbox"
name="confirmed"
[(ngModel)]="loginConfirmed"
[disabled]="loggingIn()"
required
/>
I confirm the target and understand this creates an impersonated session.</label
>
<button mat-flat-button type="submit" [disabled]="loggingIn() || loginForm.invalid">
{{ loggingIn() ? 'Creating session…' : 'Create impersonated session' }}
</button>
</form>
@if (loginError(); as message) {
<p role="alert">{{ message }}</p>
}
@if (loginMessage(); as message) {
<p role="status">{{ message }}</p>
}
@if (loginRedirect(); as redirect) {
<a
mat-stroked-button
[href]="redirect"
target="_blank"
rel="noopener noreferrer"
referrerpolicy="no-referrer"
>Open connected application</a
>
}
</details>

<form
class="activity-filters"
#filterForm="ngForm"
(ngSubmit)="filterForm.valid && applyFilters()"
aria-label="Filter activity"
>
<mat-form-field
><mat-label>Operation</mat-label
><input matInput name="operation" [(ngModel)]="filters.operation" placeholder="For example, login_as"
/></mat-form-field>
<mat-form-field
><mat-label>Actor name or email</mat-label><input matInput name="actor" [(ngModel)]="filters.name"
/></mat-form-field>
<mat-form-field
><mat-label>Outcome</mat-label
><mat-select name="outcome" [(ngModel)]="filters.outcome">
<mat-option value="">All outcomes</mat-option>
<mat-option value="started">Started</mat-option><mat-option value="succeeded">Succeeded</mat-option>
<mat-option value="denied">Denied</mat-option><mat-option value="failed">Failed</mat-option>
</mat-select></mat-form-field
>
<mat-form-field
><mat-label>Target user ID</mat-label
><input matInput name="target" [(ngModel)]="filters.target_user_id" inputmode="numeric" pattern="[0-9]+"
/></mat-form-field>
<mat-form-field
><mat-label>Block ID</mat-label
><input
matInput
name="block"
[(ngModel)]="filters.feature_configuration_id"
inputmode="numeric"
pattern="[0-9]+"
/></mat-form-field>
<mat-form-field
><mat-label>From date</mat-label><input matInput name="from" type="date" [(ngModel)]="filters.startDate"
/></mat-form-field>
<mat-form-field
><mat-label>Through date</mat-label
><input matInput name="through" type="date" [(ngModel)]="filters.endDate"
/></mat-form-field>
<div class="filter-actions">
<button mat-flat-button type="submit" [disabled]="loading() || filterForm.invalid">Apply filters</button
><button mat-button type="button" (click)="clearFilters()" [disabled]="loading()">Clear</button>
</div>
</form>

@if (loading()) {
<p role="status" aria-live="polite">Loading activity…</p>
} @else if (error(); as message) {
<div role="alert">
<p>{{ message }}</p>
<button mat-stroked-button type="button" (click)="load(page())">Retry</button>
</div>
} @else if (rows().length === 0) {
<p role="status">No activity matches these filters for the current client.</p>
} @else {
<p role="status">{{ total() }} matching operations · Page {{ page() }} of {{ totalPages() }}</p>
<div class="table-scroll" role="region" aria-label="Activity results" tabindex="0">
<table>
<caption class="visually-hidden">
Operator activity in the current client
</caption>
<thead>
<tr>
<th scope="col">When</th>
<th scope="col">Actor</th>
<th scope="col">Operation</th>
<th scope="col">Outcome</th>
<th scope="col">Audit details</th>
</tr>
</thead>
<tbody>
@for (row of rows(); track row.id) {
<tr>
<td>
{{ row.created_at | date: 'medium'
}}<small>Operation #{{ row.id }} · Session {{ row.session_id ?? 'unavailable' }}</small>
</td>
<td>
{{ row.user?.name || row.user?.email || 'Unavailable'
}}<small>{{ row.user?.email }}</small
><small
>Actor ID:
{{ row.audit_context?.actor_user_id ?? row.user?.user_id ?? 'unavailable' }}</small
>
</td>
<td>{{ row.operation }}</td>
<td>
{{ row.audit_context?.outcome || 'Not recorded' }}
@if (row.audit_context?.failure_code) {
<small>{{ row.audit_context.failure_code }}</small>
}
</td>
<td>
@if (row.audit_context; as audit) {
<dl>
<dt>Target user</dt>
<dd>{{ audit.target_user_id ?? 'Not recorded' }}</dd>
<dt>Company</dt>
<dd>{{ audit.target_company_id ?? 'Not recorded' }}</dd>
<dt>Block</dt>
<dd>{{ audit.feature_configuration_id ?? 'Not recorded' }}</dd>
<dt>Reason</dt>
<dd class="reason">{{ audit.reason || 'Not recorded' }}</dd>
<dt>Source IP</dt>
<dd>{{ audit.source_ip || 'Not recorded' }}</dd>
</dl>
} @else {
<span>No audit details were recorded for this operation.</span>
}
</td>
</tr>
}
</tbody>
</table>
</div>
<nav class="pagination" aria-label="Activity pages">
<button mat-stroked-button type="button" (click)="load(page() - 1)" [disabled]="page() <= 1">
Previous</button
><button mat-stroked-button type="button" (click)="load(page() + 1)" [disabled]="page() >= totalPages()">
Next
</button>
</nav>
}
</section>
Loading
Loading