@huloglobal/vendure-plugin-geo-block 0.1.0 → 0.2.0
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.
- package/dist/geo-block-event.entity.d.ts +22 -0
- package/dist/geo-block-event.entity.d.ts.map +1 -0
- package/dist/geo-block-event.entity.js +68 -0
- package/dist/geo-block-event.entity.js.map +1 -0
- package/dist/geo-block.controller.d.ts +42 -25
- package/dist/geo-block.controller.d.ts.map +1 -1
- package/dist/geo-block.controller.js +368 -97
- package/dist/geo-block.controller.js.map +1 -1
- package/dist/geo-regions.d.ts +38 -13
- package/dist/geo-regions.d.ts.map +1 -1
- package/dist/geo-regions.js +212 -15
- package/dist/geo-regions.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +22 -23
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +56 -38
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
- package/ui/components/geo-block.component.ts +471 -255
|
@@ -7,15 +7,19 @@ interface ChannelRow {
|
|
|
7
7
|
code: string;
|
|
8
8
|
token: string;
|
|
9
9
|
enabled: boolean;
|
|
10
|
+
mode: 'block' | 'soft';
|
|
10
11
|
allowedRegions: string[];
|
|
11
12
|
extraAllowed: string[];
|
|
12
13
|
blockedCountries: string[];
|
|
13
14
|
allowedGbRegions: string[];
|
|
15
|
+
ipAllowlist: string[];
|
|
16
|
+
blockMessage: string;
|
|
17
|
+
blockRedirectUrl: string;
|
|
18
|
+
blockLogoUrl: string;
|
|
14
19
|
resolved: { allowedCountries: string[] | null; blockedCountries: string[] };
|
|
15
20
|
}
|
|
16
21
|
|
|
17
|
-
interface
|
|
18
|
-
interface CountryDef { value: string; label: string; flag: string; }
|
|
22
|
+
interface PresetMeta { key: string; label: string; kind: string; description: string; countryCount: number | null; }
|
|
19
23
|
|
|
20
24
|
@Component({
|
|
21
25
|
selector: 'ees-geo-block',
|
|
@@ -23,7 +27,10 @@ interface CountryDef { value: string; label: string; flag: string; }
|
|
|
23
27
|
template: `
|
|
24
28
|
<vdr-page-block>
|
|
25
29
|
<vdr-action-bar>
|
|
26
|
-
<vdr-ab-left
|
|
30
|
+
<vdr-ab-left>
|
|
31
|
+
<h2>Site access</h2>
|
|
32
|
+
<p class="subtitle">Per-channel geo-restrictions, IP allowlist, audit log.</p>
|
|
33
|
+
</vdr-ab-left>
|
|
27
34
|
<vdr-ab-right>
|
|
28
35
|
<button class="btn btn-link" (click)="reload()" [disabled]="loading">
|
|
29
36
|
<clr-icon shape="refresh"></clr-icon> Refresh
|
|
@@ -32,8 +39,8 @@ interface CountryDef { value: string; label: string; flag: string; }
|
|
|
32
39
|
</vdr-action-bar>
|
|
33
40
|
</vdr-page-block>
|
|
34
41
|
|
|
35
|
-
<vdr-page-block>
|
|
36
|
-
<div class="card
|
|
42
|
+
<vdr-page-block *ngIf="!loading && current">
|
|
43
|
+
<div class="card top-bar">
|
|
37
44
|
<div class="card-block">
|
|
38
45
|
<div class="chan-row">
|
|
39
46
|
<label class="lbl">Channel</label>
|
|
@@ -44,172 +51,332 @@ interface CountryDef { value: string; label: string; flag: string; }
|
|
|
44
51
|
<span class="status-pill" [class.on]="current.enabled" [class.off]="!current.enabled">
|
|
45
52
|
{{ current.enabled ? 'GEO-BLOCK ON' : 'GEO-BLOCK OFF' }}
|
|
46
53
|
</span>
|
|
47
|
-
<button class="btn btn-sm btn-
|
|
54
|
+
<button class="btn btn-sm" [class.btn-warning]="current.enabled" [class.btn-primary]="!current.enabled" (click)="toggleEnabled()">
|
|
48
55
|
{{ current.enabled ? 'Turn off' : 'Turn on' }}
|
|
49
56
|
</button>
|
|
57
|
+
|
|
58
|
+
<span class="mode-pill" *ngIf="current.enabled" [class.mode-block]="current.mode === 'block'" [class.mode-soft]="current.mode === 'soft'">
|
|
59
|
+
{{ current.mode === 'soft' ? 'Soft block (banner)' : 'Full block' }}
|
|
60
|
+
</span>
|
|
61
|
+
|
|
62
|
+
<span class="dirty-flag" *ngIf="dirty">● Unsaved</span>
|
|
50
63
|
</div>
|
|
51
64
|
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
<div class="tabs">
|
|
66
|
+
<button class="tab" [class.active]="tab === 'rules'" (click)="tab = 'rules'">Rules</button>
|
|
67
|
+
<button class="tab" [class.active]="tab === 'message'" (click)="tab = 'message'">Block page</button>
|
|
68
|
+
<button class="tab" [class.active]="tab === 'allowlist'" (click)="tab = 'allowlist'">IP allowlist</button>
|
|
69
|
+
<button class="tab" [class.active]="tab === 'simulate'" (click)="tab = 'simulate'">Simulate</button>
|
|
70
|
+
<button class="tab" [class.active]="tab === 'stats'" (click)="tab = 'stats'; loadStats()">Stats</button>
|
|
71
|
+
</div>
|
|
56
72
|
</div>
|
|
57
73
|
</div>
|
|
58
74
|
</vdr-page-block>
|
|
59
75
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
</
|
|
76
|
+
<!-- ============================================================= RULES TAB -->
|
|
77
|
+
<ng-container *ngIf="!loading && current && tab === 'rules'">
|
|
78
|
+
<vdr-page-block>
|
|
79
|
+
<div class="card">
|
|
80
|
+
<div class="card-block">
|
|
81
|
+
<h3 class="step-title">Mode</h3>
|
|
82
|
+
<div class="mode-grid">
|
|
83
|
+
<label class="mode-card" [class.active]="current.mode === 'block'">
|
|
84
|
+
<input type="radio" name="bmode" value="block" [(ngModel)]="current.mode" (ngModelChange)="markDirty()">
|
|
85
|
+
<div class="mode-title">Full block</div>
|
|
86
|
+
<div class="mode-body">Blocked visitors never see the storefront — they get the block page (or are redirected).</div>
|
|
87
|
+
</label>
|
|
88
|
+
<label class="mode-card" [class.active]="current.mode === 'soft'">
|
|
89
|
+
<input type="radio" name="bmode" value="soft" [(ngModel)]="current.mode" (ngModelChange)="markDirty()">
|
|
90
|
+
<div class="mode-title">Soft block (browse-only)</div>
|
|
91
|
+
<div class="mode-body">Visitors can browse but a banner explains you don't ship to their country and checkout is hidden.</div>
|
|
92
|
+
</label>
|
|
93
|
+
</div>
|
|
78
94
|
</div>
|
|
79
95
|
</div>
|
|
80
|
-
</
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
</vdr-page-block>
|
|
97
|
+
|
|
98
|
+
<vdr-page-block>
|
|
99
|
+
<div class="card">
|
|
100
|
+
<div class="card-block">
|
|
101
|
+
<h3 class="step-title">Strategy</h3>
|
|
102
|
+
<div class="mode-grid">
|
|
103
|
+
<label class="mode-card" [class.active]="strategy === 'specific'">
|
|
104
|
+
<input type="radio" name="strat" value="specific" [(ngModel)]="strategy" (ngModelChange)="onStrategyChange()">
|
|
105
|
+
<div class="mode-title">Allow only specific places</div>
|
|
106
|
+
<div class="mode-body">Pick regions or individual countries — everyone else is blocked.</div>
|
|
107
|
+
</label>
|
|
108
|
+
<label class="mode-card" [class.active]="strategy === 'worldwide'">
|
|
109
|
+
<input type="radio" name="strat" value="worldwide" [(ngModel)]="strategy" (ngModelChange)="onStrategyChange()">
|
|
110
|
+
<div class="mode-title">Worldwide except blocked</div>
|
|
111
|
+
<div class="mode-body">Allow everyone except the denylist below.</div>
|
|
112
|
+
</label>
|
|
113
|
+
</div>
|
|
95
114
|
</div>
|
|
96
115
|
</div>
|
|
97
|
-
</
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
</vdr-page-block>
|
|
117
|
+
|
|
118
|
+
<vdr-page-block *ngIf="strategy === 'specific'">
|
|
119
|
+
<div class="card">
|
|
120
|
+
<div class="card-block">
|
|
121
|
+
<h3 class="step-title">Allowed regions <small>({{ pickedRegionCount() }} picked)</small></h3>
|
|
122
|
+
<p class="hint">One-click presets. Tick as many as you want — they stack.</p>
|
|
123
|
+
|
|
124
|
+
<input class="form-input filter-input" placeholder="Filter presets…" [(ngModel)]="presetFilter">
|
|
125
|
+
|
|
126
|
+
<div class="preset-section" *ngFor="let group of presetGroups">
|
|
127
|
+
<h4 class="group-title">{{ group.label }}</h4>
|
|
128
|
+
<div class="preset-grid">
|
|
129
|
+
<label *ngFor="let p of filteredPresets(group.kind)" class="preset-card" [class.active]="isRegionPicked(p.key)">
|
|
130
|
+
<input type="checkbox" [checked]="isRegionPicked(p.key)" (change)="toggleRegion(p.key)">
|
|
131
|
+
<div class="preset-label">{{ p.label }}</div>
|
|
132
|
+
<div class="preset-hint">{{ p.description }}<span *ngIf="p.countryCount"> · {{ p.countryCount }} countries</span></div>
|
|
133
|
+
</label>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
112
136
|
</div>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<
|
|
137
|
+
</div>
|
|
138
|
+
</vdr-page-block>
|
|
139
|
+
|
|
140
|
+
<vdr-page-block *ngIf="strategy === 'specific'">
|
|
141
|
+
<div class="card">
|
|
142
|
+
<div class="card-block">
|
|
143
|
+
<h3 class="step-title">Extra allowed countries <small>(optional)</small></h3>
|
|
144
|
+
<p class="hint">Add countries that aren't covered by a preset above.</p>
|
|
145
|
+
<div class="chip-row">
|
|
146
|
+
<span class="chip" *ngFor="let cc of current.extraAllowed">
|
|
147
|
+
{{ countryLabel(cc) }}
|
|
148
|
+
<button class="chip-x" (click)="removeExtra(cc)" title="Remove">×</button>
|
|
149
|
+
</span>
|
|
150
|
+
<span *ngIf="!current.extraAllowed.length" class="hint inline">None yet.</span>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="picker">
|
|
153
|
+
<input class="form-input" placeholder="Country code (e.g. JP, IL, BR)" [(ngModel)]="newExtra" (keyup.enter)="addExtra()" maxlength="2" style="text-transform: uppercase">
|
|
154
|
+
<button class="btn btn-secondary btn-sm" (click)="addExtra()" [disabled]="!newExtra">+ Add</button>
|
|
155
|
+
</div>
|
|
120
156
|
</div>
|
|
121
157
|
</div>
|
|
122
|
-
</
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<
|
|
136
|
-
</
|
|
137
|
-
<
|
|
158
|
+
</vdr-page-block>
|
|
159
|
+
|
|
160
|
+
<vdr-page-block>
|
|
161
|
+
<div class="card">
|
|
162
|
+
<div class="card-block">
|
|
163
|
+
<h3 class="step-title">Always-blocked countries</h3>
|
|
164
|
+
<p class="hint" *ngIf="strategy === 'specific'">Subtracted from the allow-list — e.g. block 🇷🇺 while allowing “Europe”.</p>
|
|
165
|
+
<p class="hint" *ngIf="strategy === 'worldwide'">In worldwide mode this is the <em>only</em> filter — everyone except these countries is allowed.</p>
|
|
166
|
+
<div class="chip-row">
|
|
167
|
+
<span class="chip blocked" *ngFor="let cc of current.blockedCountries">
|
|
168
|
+
{{ countryLabel(cc) }}
|
|
169
|
+
<button class="chip-x" (click)="removeBlocked(cc)" title="Remove">×</button>
|
|
170
|
+
</span>
|
|
171
|
+
<span *ngIf="!current.blockedCountries.length" class="hint inline">None yet.</span>
|
|
172
|
+
</div>
|
|
173
|
+
<div class="picker">
|
|
174
|
+
<input class="form-input" placeholder="Country code (e.g. RU, IR)" [(ngModel)]="newBlocked" (keyup.enter)="addBlocked()" maxlength="2" style="text-transform: uppercase">
|
|
175
|
+
<button class="btn btn-secondary btn-sm" (click)="addBlocked()" [disabled]="!newBlocked">+ Add</button>
|
|
176
|
+
<span class="hint inline" style="margin-left: 12px">Common: RU, BY, IR, KP, SY, CU, MM</span>
|
|
177
|
+
</div>
|
|
138
178
|
</div>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
<
|
|
179
|
+
</div>
|
|
180
|
+
</vdr-page-block>
|
|
181
|
+
|
|
182
|
+
<vdr-page-block *ngIf="isUkResolved()">
|
|
183
|
+
<div class="card">
|
|
184
|
+
<div class="card-block">
|
|
185
|
+
<h3 class="step-title">UK subdivisions <small>(only applies when GB is allowed)</small></h3>
|
|
186
|
+
<p class="hint">Tighten to specific UK regions. Leave empty or pick all four to allow the whole UK.</p>
|
|
187
|
+
<div class="uk-row">
|
|
188
|
+
<label *ngFor="let r of ukRegions" class="uk-pill" [class.active]="current.allowedGbRegions.includes(r.value)">
|
|
189
|
+
<input type="checkbox" [checked]="current.allowedGbRegions.includes(r.value)" (change)="toggleUkRegion(r.value)">
|
|
190
|
+
{{ r.label }}
|
|
191
|
+
</label>
|
|
192
|
+
</div>
|
|
146
193
|
</div>
|
|
147
194
|
</div>
|
|
148
|
-
</
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
195
|
+
</vdr-page-block>
|
|
196
|
+
|
|
197
|
+
<vdr-page-block>
|
|
198
|
+
<div class="card preview-card">
|
|
199
|
+
<div class="card-block">
|
|
200
|
+
<h3 class="step-title">Resolved allow-list</h3>
|
|
201
|
+
<div *ngIf="!current.enabled" class="preview-banner preview-off">
|
|
202
|
+
<strong>Geo-block is OFF</strong> — everyone can visit.
|
|
203
|
+
</div>
|
|
204
|
+
<div *ngIf="current.enabled">
|
|
205
|
+
<div class="preview-banner preview-allow">
|
|
206
|
+
<strong *ngIf="resolvedAllowed() === null">✅ Allow visitors from anywhere</strong>
|
|
207
|
+
<strong *ngIf="resolvedAllowed() !== null && resolvedAllowed()!.length">
|
|
208
|
+
✅ Allow visitors from {{ resolvedAllowed()!.length }} {{ resolvedAllowed()!.length === 1 ? 'country' : 'countries' }}
|
|
209
|
+
</strong>
|
|
210
|
+
<strong *ngIf="resolvedAllowed() !== null && !resolvedAllowed()!.length" class="warn">
|
|
211
|
+
⚠️ Nothing is allowed — every visitor will be blocked.
|
|
212
|
+
</strong>
|
|
213
|
+
<div class="country-chips" *ngIf="resolvedAllowed() !== null && resolvedAllowed()!.length">
|
|
214
|
+
<span class="mini-chip" *ngFor="let cc of resolvedAllowed()!">{{ cc }}</span>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="preview-banner preview-block" *ngIf="current.blockedCountries.length">
|
|
218
|
+
<strong>🚫 Always block</strong>
|
|
219
|
+
<div class="country-chips">
|
|
220
|
+
<span class="mini-chip blocked" *ngFor="let cc of current.blockedCountries">{{ cc }}</span>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
164
224
|
</div>
|
|
165
225
|
</div>
|
|
166
|
-
</
|
|
167
|
-
</
|
|
226
|
+
</vdr-page-block>
|
|
227
|
+
</ng-container>
|
|
228
|
+
|
|
229
|
+
<!-- ============================================================= BLOCK PAGE TAB -->
|
|
230
|
+
<ng-container *ngIf="!loading && current && tab === 'message'">
|
|
231
|
+
<vdr-page-block>
|
|
232
|
+
<div class="card">
|
|
233
|
+
<div class="card-block">
|
|
234
|
+
<h3 class="step-title">Block page</h3>
|
|
235
|
+
<p class="hint">Customise what blocked visitors see. Leave blank for sensible defaults.</p>
|
|
236
|
+
|
|
237
|
+
<div class="form-row">
|
|
238
|
+
<label>Custom message <small>(optional)</small></label>
|
|
239
|
+
<textarea class="form-input" rows="4" [(ngModel)]="current.blockMessage" (ngModelChange)="markDirty()" placeholder="We're sorry — we don't ship to your country yet. Get in touch if you'd like to be notified when we expand."></textarea>
|
|
240
|
+
</div>
|
|
168
241
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
242
|
+
<div class="form-row">
|
|
243
|
+
<label>Redirect URL <small>(optional)</small></label>
|
|
244
|
+
<input class="form-input" [(ngModel)]="current.blockRedirectUrl" (ngModelChange)="markDirty()" placeholder="https://example.com/sorry">
|
|
245
|
+
<p class="hint">When set, blocked visitors are redirected here instead of seeing the block page.</p>
|
|
246
|
+
</div>
|
|
173
247
|
|
|
174
|
-
|
|
175
|
-
|
|
248
|
+
<div class="form-row">
|
|
249
|
+
<label>Logo URL <small>(optional)</small></label>
|
|
250
|
+
<input class="form-input" [(ngModel)]="current.blockLogoUrl" (ngModelChange)="markDirty()" placeholder="https://example.com/logo.svg">
|
|
251
|
+
</div>
|
|
176
252
|
</div>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
<
|
|
253
|
+
</div>
|
|
254
|
+
</vdr-page-block>
|
|
255
|
+
</ng-container>
|
|
256
|
+
|
|
257
|
+
<!-- ============================================================= IP ALLOWLIST TAB -->
|
|
258
|
+
<ng-container *ngIf="!loading && current && tab === 'allowlist'">
|
|
259
|
+
<vdr-page-block>
|
|
260
|
+
<div class="card">
|
|
261
|
+
<div class="card-block">
|
|
262
|
+
<h3 class="step-title">IP allowlist <small>(overrides every rule)</small></h3>
|
|
263
|
+
<p class="hint">IPs or IPv4 CIDR ranges that bypass all country / region rules. Use for your office, oncall engineers, payment processor probes.</p>
|
|
264
|
+
<div class="chip-row">
|
|
265
|
+
<span class="chip mono" *ngFor="let ip of current.ipAllowlist">
|
|
266
|
+
{{ ip }}
|
|
267
|
+
<button class="chip-x" (click)="removeIp(ip)" title="Remove">×</button>
|
|
268
|
+
</span>
|
|
269
|
+
<span *ngIf="!current.ipAllowlist.length" class="hint inline">No bypass IPs configured.</span>
|
|
270
|
+
</div>
|
|
271
|
+
<div class="picker">
|
|
272
|
+
<input class="form-input mono" placeholder="203.0.113.42 or 203.0.113.0/24" [(ngModel)]="newIp" (keyup.enter)="addIp()" style="min-width: 260px">
|
|
273
|
+
<button class="btn btn-secondary btn-sm" (click)="addIp()" [disabled]="!newIp">+ Add</button>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</vdr-page-block>
|
|
278
|
+
</ng-container>
|
|
279
|
+
|
|
280
|
+
<!-- ============================================================= SIMULATE TAB -->
|
|
281
|
+
<ng-container *ngIf="!loading && current && tab === 'simulate'">
|
|
282
|
+
<vdr-page-block>
|
|
283
|
+
<div class="card">
|
|
284
|
+
<div class="card-block">
|
|
285
|
+
<h3 class="step-title">Simulate a visitor</h3>
|
|
286
|
+
<p class="hint">Test exactly what your current rules will do for a hypothetical visitor — without saving anything to the storefront.</p>
|
|
287
|
+
<div class="sim-grid">
|
|
288
|
+
<div>
|
|
289
|
+
<label>Country code</label>
|
|
290
|
+
<input class="form-input" [(ngModel)]="sim.country" placeholder="US" maxlength="2" style="text-transform: uppercase">
|
|
291
|
+
</div>
|
|
292
|
+
<div>
|
|
293
|
+
<label>UK region <small>(optional)</small></label>
|
|
294
|
+
<input class="form-input" [(ngModel)]="sim.region" placeholder="ENG / WLS / SCT / NIR" maxlength="3" style="text-transform: uppercase">
|
|
295
|
+
</div>
|
|
296
|
+
<div>
|
|
297
|
+
<label>IP address <small>(optional)</small></label>
|
|
298
|
+
<input class="form-input" [(ngModel)]="sim.ip" placeholder="203.0.113.42">
|
|
192
299
|
</div>
|
|
193
300
|
</div>
|
|
301
|
+
<button class="btn btn-primary" (click)="runSim()" [disabled]="simBusy">
|
|
302
|
+
{{ simBusy ? 'Running…' : 'Run simulation' }}
|
|
303
|
+
</button>
|
|
194
304
|
|
|
195
|
-
<div class="
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
305
|
+
<div class="sim-result" *ngIf="simResult">
|
|
306
|
+
<div *ngIf="simResult.ipMatchesAllowlist" class="sim-banner allow">
|
|
307
|
+
<strong>✅ Allowed</strong> — IP matches the allowlist, every other rule is bypassed.
|
|
308
|
+
</div>
|
|
309
|
+
<div *ngIf="!simResult.ipMatchesAllowlist && simResult.verdict.allowed" class="sim-banner allow">
|
|
310
|
+
<strong>✅ Allowed</strong> ({{ simResult.verdict.reason }})
|
|
311
|
+
</div>
|
|
312
|
+
<div *ngIf="!simResult.ipMatchesAllowlist && !simResult.verdict.allowed" class="sim-banner deny">
|
|
313
|
+
<strong>🚫 Blocked</strong> ({{ simResult.verdict.reason }})
|
|
199
314
|
</div>
|
|
200
315
|
</div>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</vdr-page-block>
|
|
319
|
+
</ng-container>
|
|
320
|
+
|
|
321
|
+
<!-- ============================================================= STATS TAB -->
|
|
322
|
+
<ng-container *ngIf="!loading && current && tab === 'stats'">
|
|
323
|
+
<vdr-page-block>
|
|
324
|
+
<div class="card">
|
|
325
|
+
<div class="card-block">
|
|
326
|
+
<h3 class="step-title">Block statistics <small>last {{ statsDays }} days</small></h3>
|
|
327
|
+
|
|
328
|
+
<div *ngIf="!stats" class="hint">Loading…</div>
|
|
329
|
+
<div *ngIf="stats">
|
|
330
|
+
<div class="stats-grid">
|
|
331
|
+
<div class="stat-card">
|
|
332
|
+
<div class="num">{{ stats.totals.blocked || 0 }}</div>
|
|
333
|
+
<div class="lbl">Full blocks</div>
|
|
334
|
+
</div>
|
|
335
|
+
<div class="stat-card">
|
|
336
|
+
<div class="num">{{ stats.totals.softBlocked || 0 }}</div>
|
|
337
|
+
<div class="lbl">Soft blocks</div>
|
|
338
|
+
</div>
|
|
339
|
+
<div class="stat-card">
|
|
340
|
+
<div class="num">{{ stats.totals.total || 0 }}</div>
|
|
341
|
+
<div class="lbl">Total events</div>
|
|
342
|
+
</div>
|
|
343
|
+
<div class="stat-card">
|
|
344
|
+
<div class="num">{{ stats.totals.uniqueIps || 0 }}</div>
|
|
345
|
+
<div class="lbl">Unique IPs</div>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
201
348
|
|
|
202
|
-
|
|
203
|
-
<
|
|
204
|
-
|
|
349
|
+
<h4 style="margin-top: 24px">Top blocked countries</h4>
|
|
350
|
+
<table class="table table-compact" *ngIf="stats.topCountries?.length">
|
|
351
|
+
<thead><tr><th>Country</th><th style="width: 100px">Blocked</th></tr></thead>
|
|
352
|
+
<tbody>
|
|
353
|
+
<tr *ngFor="let r of stats.topCountries">
|
|
354
|
+
<td>{{ r.country || '—' }}</td>
|
|
355
|
+
<td>{{ r.n }}</td>
|
|
356
|
+
</tr>
|
|
357
|
+
</tbody>
|
|
358
|
+
</table>
|
|
359
|
+
<p *ngIf="!stats.topCountries?.length" class="hint">No blocks recorded yet.</p>
|
|
360
|
+
|
|
361
|
+
<h4 style="margin-top: 24px">By reason</h4>
|
|
362
|
+
<table class="table table-compact" *ngIf="stats.reasons?.length">
|
|
363
|
+
<thead><tr><th>Reason</th><th style="width: 100px">Count</th></tr></thead>
|
|
364
|
+
<tbody>
|
|
365
|
+
<tr *ngFor="let r of stats.reasons">
|
|
366
|
+
<td>{{ r.reason }}</td>
|
|
367
|
+
<td>{{ r.n }}</td>
|
|
368
|
+
</tr>
|
|
369
|
+
</tbody>
|
|
370
|
+
</table>
|
|
205
371
|
</div>
|
|
206
372
|
</div>
|
|
207
373
|
</div>
|
|
208
|
-
</
|
|
209
|
-
</
|
|
374
|
+
</vdr-page-block>
|
|
375
|
+
</ng-container>
|
|
210
376
|
|
|
211
|
-
|
|
212
|
-
|
|
377
|
+
<!-- ============================================================= SAVE BAR -->
|
|
378
|
+
<vdr-page-block *ngIf="!loading && current && tab !== 'simulate' && tab !== 'stats'">
|
|
379
|
+
<div class="save-bar">
|
|
213
380
|
<button class="btn btn-primary" (click)="save()" [disabled]="saving">
|
|
214
381
|
{{ saving ? 'Saving…' : 'Save changes' }}
|
|
215
382
|
</button>
|
|
@@ -220,26 +387,55 @@ interface CountryDef { value: string; label: string; flag: string; }
|
|
|
220
387
|
`,
|
|
221
388
|
styles: [`
|
|
222
389
|
:host { color: var(--color-text-100, inherit); display: block; }
|
|
390
|
+
.subtitle { font-size: 13px; color: var(--color-component-color-300); margin: 2px 0 0; }
|
|
391
|
+
|
|
392
|
+
.top-bar { border-top: 3px solid var(--color-primary-500, #1d4ed8); }
|
|
223
393
|
.chan-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
|
224
|
-
.
|
|
225
|
-
.form-select {
|
|
226
|
-
padding: 6px 10px; border-radius: 4px; min-width:
|
|
394
|
+
.lbl { font-size: 12px; color: var(--color-component-color-300); }
|
|
395
|
+
.form-select, .form-input {
|
|
396
|
+
padding: 6px 10px; border-radius: 4px; min-width: 180px;
|
|
227
397
|
border: 1px solid var(--color-component-border-200);
|
|
228
398
|
background: var(--color-component-bg-100);
|
|
229
399
|
color: var(--color-text-100, inherit);
|
|
230
400
|
}
|
|
401
|
+
.form-input.mono { font-family: var(--clr-font-family-monospace, monospace); }
|
|
402
|
+
.filter-input { width: 100%; max-width: 360px; margin-bottom: 12px; }
|
|
403
|
+
textarea.form-input { font-family: inherit; min-height: 80px; width: 100%; max-width: 600px; }
|
|
404
|
+
.form-row { margin: 12px 0; }
|
|
405
|
+
.form-row label { display: block; font-weight: 600; font-size: 13px; margin-bottom: 4px; }
|
|
406
|
+
.form-row label small { color: var(--color-component-color-300); font-weight: 400; margin-left: 4px; }
|
|
407
|
+
|
|
231
408
|
.status-pill {
|
|
232
409
|
display: inline-block; padding: 3px 12px; border-radius: 12px;
|
|
233
410
|
font-size: 11px; font-weight: 700; letter-spacing: 0.5px;
|
|
234
411
|
}
|
|
235
412
|
.status-pill.on { background: #10b981; color: #fff; }
|
|
236
413
|
.status-pill.off { background: var(--color-component-bg-200); color: var(--color-component-color-300); }
|
|
414
|
+
.mode-pill {
|
|
415
|
+
display: inline-block; padding: 3px 10px; border-radius: 10px;
|
|
416
|
+
font-size: 11px; font-weight: 600;
|
|
417
|
+
}
|
|
418
|
+
.mode-pill.mode-block { background: #fee2e2; color: #991b1b; }
|
|
419
|
+
.mode-pill.mode-soft { background: #fef3c7; color: #92400e; }
|
|
420
|
+
.dirty-flag { color: #f59e0b; font-weight: 600; font-size: 12px; }
|
|
421
|
+
|
|
422
|
+
.tabs { display: flex; gap: 4px; margin-top: 16px; border-bottom: 1px solid var(--color-component-border-200); }
|
|
423
|
+
.tab {
|
|
424
|
+
padding: 8px 16px;
|
|
425
|
+
background: transparent; border: 0; border-bottom: 2px solid transparent;
|
|
426
|
+
font-size: 13px; font-weight: 500;
|
|
427
|
+
color: var(--color-component-color-300); cursor: pointer;
|
|
428
|
+
margin-bottom: -1px;
|
|
429
|
+
}
|
|
430
|
+
.tab:hover { color: var(--color-text-100); }
|
|
431
|
+
.tab.active { border-bottom-color: var(--color-primary-500, #1d4ed8); color: var(--color-text-100); font-weight: 600; }
|
|
237
432
|
|
|
238
433
|
.hint { font-size: 13px; color: var(--color-component-color-300); margin: 6px 0 12px; }
|
|
239
434
|
.hint.inline { display: inline; margin: 0; }
|
|
240
435
|
|
|
241
436
|
.step-title { font-size: 16px; font-weight: 600; margin-bottom: 4px; }
|
|
242
|
-
.step-title small { color: var(--color-component-color-300); font-weight: 400; }
|
|
437
|
+
.step-title small { color: var(--color-component-color-300); font-weight: 400; margin-left: 6px; }
|
|
438
|
+
.group-title { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: var(--color-component-color-300); margin: 14px 0 8px; }
|
|
243
439
|
|
|
244
440
|
.mode-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
|
|
245
441
|
.mode-card {
|
|
@@ -254,6 +450,7 @@ interface CountryDef { value: string; label: string; flag: string; }
|
|
|
254
450
|
.mode-card .mode-title { font-weight: 600; font-size: 14px; margin-bottom: 4px; }
|
|
255
451
|
.mode-card .mode-body { font-size: 12px; color: var(--color-component-color-300); line-height: 1.5; }
|
|
256
452
|
|
|
453
|
+
.preset-section { margin-top: 8px; }
|
|
257
454
|
.preset-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; }
|
|
258
455
|
.preset-card {
|
|
259
456
|
display: block; padding: 12px;
|
|
@@ -264,7 +461,7 @@ interface CountryDef { value: string; label: string; flag: string; }
|
|
|
264
461
|
.preset-card.active { border-color: var(--color-primary-500, #1d4ed8); background: var(--color-component-bg-200); }
|
|
265
462
|
.preset-card input { float: right; }
|
|
266
463
|
.preset-label { font-weight: 600; font-size: 13px; }
|
|
267
|
-
.preset-hint { font-size: 11px; color: var(--color-component-color-300); margin-top: 4px; }
|
|
464
|
+
.preset-hint { font-size: 11px; color: var(--color-component-color-300); margin-top: 4px; line-height: 1.4; }
|
|
268
465
|
|
|
269
466
|
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; min-height: 30px; }
|
|
270
467
|
.chip {
|
|
@@ -274,6 +471,7 @@ interface CountryDef { value: string; label: string; flag: string; }
|
|
|
274
471
|
border: 1px solid #93c5fd;
|
|
275
472
|
}
|
|
276
473
|
.chip.blocked { background: #fee2e2; color: #991b1b; border-color: #fca5a5; }
|
|
474
|
+
.chip.mono { font-family: var(--clr-font-family-monospace, monospace); background: var(--color-component-bg-200); color: var(--color-text-100); border-color: var(--color-component-border-200); }
|
|
277
475
|
.chip-x {
|
|
278
476
|
background: transparent; border: none; cursor: pointer; padding: 0 0 0 2px;
|
|
279
477
|
color: inherit; font-size: 16px; line-height: 1;
|
|
@@ -304,6 +502,24 @@ interface CountryDef { value: string; label: string; flag: string; }
|
|
|
304
502
|
font-family: var(--clr-font-family-monospace, monospace);
|
|
305
503
|
}
|
|
306
504
|
.mini-chip.blocked { background: #fee2e2; color: #991b1b; border-color: #fca5a5; }
|
|
505
|
+
|
|
506
|
+
.sim-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 12px 0 16px; max-width: 700px; }
|
|
507
|
+
.sim-result { margin-top: 16px; }
|
|
508
|
+
.sim-banner { padding: 12px 16px; border-radius: 6px; font-size: 14px; }
|
|
509
|
+
.sim-banner.allow { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
|
|
510
|
+
.sim-banner.deny { background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; }
|
|
511
|
+
|
|
512
|
+
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; }
|
|
513
|
+
.stat-card {
|
|
514
|
+
padding: 14px 18px;
|
|
515
|
+
border: 1px solid var(--color-component-border-200);
|
|
516
|
+
border-radius: 6px;
|
|
517
|
+
background: var(--color-component-bg-100);
|
|
518
|
+
}
|
|
519
|
+
.stat-card .num { font-size: 24px; font-weight: 700; line-height: 1.2; color: var(--color-primary-500, #1d4ed8); }
|
|
520
|
+
.stat-card .lbl { font-size: 11px; color: var(--color-component-color-300); margin-top: 2px; }
|
|
521
|
+
|
|
522
|
+
.save-bar { display: flex; gap: 8px; align-items: center; padding: 12px; background: var(--color-component-bg-100); border: 1px solid var(--color-component-border-200); border-radius: 6px; }
|
|
307
523
|
`],
|
|
308
524
|
})
|
|
309
525
|
export class GeoBlockComponent implements OnInit {
|
|
@@ -312,61 +528,30 @@ export class GeoBlockComponent implements OnInit {
|
|
|
312
528
|
channels: ChannelRow[] = [];
|
|
313
529
|
currentToken = '';
|
|
314
530
|
current: ChannelRow | null = null;
|
|
315
|
-
/** UI mode derived from underlying fields. */
|
|
316
|
-
mode: 'specific' | 'worldwide' = 'specific';
|
|
317
531
|
dirty = false;
|
|
318
|
-
newExtra = '';
|
|
319
|
-
newBlocked = '';
|
|
320
532
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
{ value: 'UK_ONLY', label: '🇬🇧 United Kingdom only', hint: 'Just GB.', countries: 1 },
|
|
325
|
-
{ value: 'BRITISH_ISLES', label: '🏴 British Isles', hint: 'UK, Ireland, Isle of Man, Channel Islands, Faroes.', countries: 6 },
|
|
326
|
-
{ value: 'EU', label: '🇪🇺 European Union (27)', hint: 'All current EU member states.', countries: 27 },
|
|
327
|
-
{ value: 'EEA', label: '🇪🇺 EEA', hint: 'EU + Iceland, Liechtenstein, Norway.', countries: 30 },
|
|
328
|
-
{ value: 'EUROPE', label: '🌍 All of Europe', hint: 'Whole continent incl. UK, RU, UA, micro-states.', countries: 51 },
|
|
329
|
-
{ value: 'NORTH_AMERICA', label: '🌎 North America', hint: 'US, Canada, Mexico.', countries: 3 },
|
|
330
|
-
{ value: 'OCEANIA', label: '🌏 Oceania', hint: 'Australia, New Zealand.', countries: 2 },
|
|
331
|
-
];
|
|
533
|
+
tab: 'rules' | 'message' | 'allowlist' | 'simulate' | 'stats' = 'rules';
|
|
534
|
+
strategy: 'specific' | 'worldwide' = 'specific';
|
|
535
|
+
presetFilter = '';
|
|
332
536
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
{
|
|
347
|
-
{
|
|
348
|
-
{
|
|
349
|
-
{
|
|
350
|
-
{
|
|
351
|
-
{ value: 'FI', label: 'Finland', flag: '🇫🇮' },
|
|
352
|
-
{ value: 'IS', label: 'Iceland', flag: '🇮🇸' },
|
|
353
|
-
{ value: 'PL', label: 'Poland', flag: '🇵🇱' },
|
|
354
|
-
{ value: 'CZ', label: 'Czechia', flag: '🇨🇿' },
|
|
355
|
-
{ value: 'US', label: 'United States', flag: '🇺🇸' },
|
|
356
|
-
{ value: 'CA', label: 'Canada', flag: '🇨🇦' },
|
|
357
|
-
{ value: 'AU', label: 'Australia', flag: '🇦🇺' },
|
|
358
|
-
{ value: 'NZ', label: 'New Zealand', flag: '🇳🇿' },
|
|
359
|
-
{ value: 'JP', label: 'Japan', flag: '🇯🇵' },
|
|
360
|
-
{ value: 'IL', label: 'Israel', flag: '🇮🇱' },
|
|
361
|
-
// Sanction-list defaults — useful in the block picker.
|
|
362
|
-
{ value: 'RU', label: 'Russia', flag: '🇷🇺' },
|
|
363
|
-
{ value: 'BY', label: 'Belarus', flag: '🇧🇾' },
|
|
364
|
-
{ value: 'UA', label: 'Ukraine', flag: '🇺🇦' },
|
|
365
|
-
{ value: 'IR', label: 'Iran', flag: '🇮🇷' },
|
|
366
|
-
{ value: 'KP', label: 'North Korea', flag: '🇰🇵' },
|
|
367
|
-
{ value: 'SY', label: 'Syria', flag: '🇸🇾' },
|
|
368
|
-
{ value: 'CU', label: 'Cuba', flag: '🇨🇺' },
|
|
369
|
-
{ value: 'CN', label: 'China', flag: '🇨🇳' },
|
|
537
|
+
newExtra = '';
|
|
538
|
+
newBlocked = '';
|
|
539
|
+
newIp = '';
|
|
540
|
+
|
|
541
|
+
sim = { country: '', region: '', ip: '' };
|
|
542
|
+
simBusy = false;
|
|
543
|
+
simResult: any = null;
|
|
544
|
+
|
|
545
|
+
stats: any = null;
|
|
546
|
+
statsDays = 30;
|
|
547
|
+
|
|
548
|
+
presets: PresetMeta[] = [];
|
|
549
|
+
presetGroups = [
|
|
550
|
+
{ kind: 'all', label: 'Everywhere' },
|
|
551
|
+
{ kind: 'geography', label: 'By geography' },
|
|
552
|
+
{ kind: 'trade', label: 'Trade blocs' },
|
|
553
|
+
{ kind: 'political', label: 'Political / economic groups' },
|
|
554
|
+
{ kind: 'language', label: 'Language / cultural' },
|
|
370
555
|
];
|
|
371
556
|
|
|
372
557
|
ukRegions = [
|
|
@@ -382,19 +567,32 @@ export class GeoBlockComponent implements OnInit {
|
|
|
382
567
|
private cdr: ChangeDetectorRef,
|
|
383
568
|
) {}
|
|
384
569
|
|
|
385
|
-
ngOnInit() {
|
|
570
|
+
ngOnInit() {
|
|
571
|
+
this.http.get<{ presets: PresetMeta[] }>('/geo-block/presets').subscribe({
|
|
572
|
+
next: r => { this.presets = r.presets || []; this.cdr.markForCheck(); },
|
|
573
|
+
error: () => { /* presets are nice-to-have, not required */ },
|
|
574
|
+
});
|
|
575
|
+
this.reload();
|
|
576
|
+
}
|
|
386
577
|
|
|
387
578
|
reload() {
|
|
388
579
|
this.loading = true;
|
|
389
580
|
this.dirty = false;
|
|
390
|
-
this.http.get<{ channels: ChannelRow[] }>('/
|
|
581
|
+
this.http.get<{ channels: ChannelRow[] }>('/geo-block/admin/channels').subscribe({
|
|
391
582
|
next: (res) => {
|
|
392
|
-
this.channels = res.channels || []
|
|
583
|
+
this.channels = (res.channels || []).map(c => ({
|
|
584
|
+
...c,
|
|
585
|
+
mode: c.mode || 'block',
|
|
586
|
+
ipAllowlist: c.ipAllowlist || [],
|
|
587
|
+
blockMessage: c.blockMessage || '',
|
|
588
|
+
blockRedirectUrl: c.blockRedirectUrl || '',
|
|
589
|
+
blockLogoUrl: c.blockLogoUrl || '',
|
|
590
|
+
}));
|
|
393
591
|
if (!this.currentToken && this.channels.length) {
|
|
394
592
|
this.currentToken = this.channels[0].token;
|
|
395
593
|
}
|
|
396
594
|
this.current = this.channels.find(c => c.token === this.currentToken) || null;
|
|
397
|
-
this.
|
|
595
|
+
this.deriveStrategy();
|
|
398
596
|
this.loading = false;
|
|
399
597
|
this.cdr.markForCheck();
|
|
400
598
|
},
|
|
@@ -408,24 +606,24 @@ export class GeoBlockComponent implements OnInit {
|
|
|
408
606
|
onChannelChange() {
|
|
409
607
|
this.current = this.channels.find(c => c.token === this.currentToken) || null;
|
|
410
608
|
this.dirty = false;
|
|
411
|
-
this.
|
|
609
|
+
this.deriveStrategy();
|
|
610
|
+
this.stats = null;
|
|
611
|
+
this.simResult = null;
|
|
412
612
|
}
|
|
413
613
|
|
|
414
|
-
private
|
|
614
|
+
private deriveStrategy() {
|
|
415
615
|
if (!this.current) return;
|
|
416
|
-
this.
|
|
616
|
+
this.strategy = this.current.allowedRegions.includes('WORLDWIDE') ? 'worldwide' : 'specific';
|
|
417
617
|
}
|
|
418
618
|
|
|
419
|
-
|
|
619
|
+
onStrategyChange() {
|
|
420
620
|
if (!this.current) return;
|
|
421
|
-
if (this.
|
|
422
|
-
// Replace presets with just WORLDWIDE (the resolver short-circuits).
|
|
621
|
+
if (this.strategy === 'worldwide') {
|
|
423
622
|
this.current.allowedRegions = ['WORLDWIDE'];
|
|
424
623
|
this.current.extraAllowed = [];
|
|
425
624
|
} else {
|
|
426
625
|
this.current.allowedRegions = this.current.allowedRegions.filter(r => r !== 'WORLDWIDE');
|
|
427
626
|
if (!this.current.allowedRegions.length && !this.current.extraAllowed.length) {
|
|
428
|
-
// Sensible default when switching back.
|
|
429
627
|
this.current.allowedRegions = ['UK_ONLY'];
|
|
430
628
|
}
|
|
431
629
|
}
|
|
@@ -452,17 +650,22 @@ export class GeoBlockComponent implements OnInit {
|
|
|
452
650
|
this.markDirty();
|
|
453
651
|
}
|
|
454
652
|
|
|
455
|
-
|
|
456
|
-
return this.
|
|
653
|
+
pickedRegionCount(): number {
|
|
654
|
+
return this.current?.allowedRegions.length || 0;
|
|
457
655
|
}
|
|
458
|
-
|
|
459
|
-
|
|
656
|
+
|
|
657
|
+
filteredPresets(kind: string): PresetMeta[] {
|
|
658
|
+
const filter = this.presetFilter.trim().toLowerCase();
|
|
659
|
+
return this.presets
|
|
660
|
+
.filter(p => p.kind === kind)
|
|
661
|
+
.filter(p => !filter || p.label.toLowerCase().includes(filter) || p.description.toLowerCase().includes(filter));
|
|
460
662
|
}
|
|
461
663
|
|
|
462
664
|
addExtra() {
|
|
463
665
|
if (!this.current || !this.newExtra) return;
|
|
464
|
-
|
|
465
|
-
|
|
666
|
+
const cc = this.newExtra.trim().toUpperCase();
|
|
667
|
+
if (cc.length === 2 && !this.current.extraAllowed.includes(cc)) {
|
|
668
|
+
this.current.extraAllowed = [...this.current.extraAllowed, cc];
|
|
466
669
|
this.markDirty();
|
|
467
670
|
}
|
|
468
671
|
this.newExtra = '';
|
|
@@ -475,8 +678,9 @@ export class GeoBlockComponent implements OnInit {
|
|
|
475
678
|
|
|
476
679
|
addBlocked() {
|
|
477
680
|
if (!this.current || !this.newBlocked) return;
|
|
478
|
-
|
|
479
|
-
|
|
681
|
+
const cc = this.newBlocked.trim().toUpperCase();
|
|
682
|
+
if (cc.length === 2 && !this.current.blockedCountries.includes(cc)) {
|
|
683
|
+
this.current.blockedCountries = [...this.current.blockedCountries, cc];
|
|
480
684
|
this.markDirty();
|
|
481
685
|
}
|
|
482
686
|
this.newBlocked = '';
|
|
@@ -487,6 +691,21 @@ export class GeoBlockComponent implements OnInit {
|
|
|
487
691
|
this.markDirty();
|
|
488
692
|
}
|
|
489
693
|
|
|
694
|
+
addIp() {
|
|
695
|
+
if (!this.current || !this.newIp) return;
|
|
696
|
+
const ip = this.newIp.trim();
|
|
697
|
+
if (ip && !this.current.ipAllowlist.includes(ip)) {
|
|
698
|
+
this.current.ipAllowlist = [...this.current.ipAllowlist, ip];
|
|
699
|
+
this.markDirty();
|
|
700
|
+
}
|
|
701
|
+
this.newIp = '';
|
|
702
|
+
}
|
|
703
|
+
removeIp(ip: string) {
|
|
704
|
+
if (!this.current) return;
|
|
705
|
+
this.current.ipAllowlist = this.current.ipAllowlist.filter(i => i !== ip);
|
|
706
|
+
this.markDirty();
|
|
707
|
+
}
|
|
708
|
+
|
|
490
709
|
toggleUkRegion(r: string) {
|
|
491
710
|
if (!this.current) return;
|
|
492
711
|
if (this.current.allowedGbRegions.includes(r)) {
|
|
@@ -502,34 +721,17 @@ export class GeoBlockComponent implements OnInit {
|
|
|
502
721
|
return allowed === null || allowed.includes('GB');
|
|
503
722
|
}
|
|
504
723
|
|
|
505
|
-
|
|
506
|
-
return this.countryDefs.find(c => c.value === cc)?.flag || cc;
|
|
507
|
-
}
|
|
508
|
-
countryLabel(cc: string): string {
|
|
509
|
-
return this.countryDefs.find(c => c.value === cc)?.label || cc;
|
|
510
|
-
}
|
|
511
|
-
ukLabel(r: string): string {
|
|
512
|
-
return this.ukRegions.find(u => u.value === r)?.label || r;
|
|
513
|
-
}
|
|
724
|
+
countryLabel(cc: string): string { return cc; }
|
|
514
725
|
|
|
515
|
-
/**
|
|
516
|
-
*
|
|
726
|
+
/** Local preview — uses the server-resolved allowed list when no
|
|
727
|
+
* rule changes are pending. Best-effort otherwise. */
|
|
517
728
|
resolvedAllowed(): string[] | null {
|
|
518
729
|
if (!this.current) return [];
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const RM = REGION_TO_COUNTRIES;
|
|
522
|
-
const set = new Set<string>();
|
|
523
|
-
for (const r of regions) {
|
|
524
|
-
const cs = RM[r as keyof typeof RM];
|
|
525
|
-
if (cs) for (const c of cs) set.add(c);
|
|
526
|
-
}
|
|
527
|
-
for (const c of this.current.extraAllowed) set.add(c.toUpperCase());
|
|
528
|
-
for (const c of this.current.blockedCountries) set.delete(c.toUpperCase());
|
|
529
|
-
return Array.from(set).sort();
|
|
730
|
+
if (this.current.allowedRegions.includes('WORLDWIDE')) return null;
|
|
731
|
+
return this.current.resolved?.allowedCountries ?? null;
|
|
530
732
|
}
|
|
531
733
|
|
|
532
|
-
|
|
734
|
+
markDirty() { this.dirty = true; }
|
|
533
735
|
|
|
534
736
|
save() {
|
|
535
737
|
if (!this.current) return;
|
|
@@ -537,16 +739,22 @@ export class GeoBlockComponent implements OnInit {
|
|
|
537
739
|
const body = {
|
|
538
740
|
token: this.current.token,
|
|
539
741
|
enabled: this.current.enabled,
|
|
742
|
+
mode: this.current.mode,
|
|
540
743
|
allowedRegions: this.current.allowedRegions,
|
|
541
744
|
extraAllowed: this.current.extraAllowed,
|
|
542
745
|
blockedCountries: this.current.blockedCountries,
|
|
543
746
|
allowedGbRegions: this.current.allowedGbRegions,
|
|
747
|
+
ipAllowlist: this.current.ipAllowlist,
|
|
748
|
+
blockMessage: this.current.blockMessage,
|
|
749
|
+
blockRedirectUrl: this.current.blockRedirectUrl,
|
|
750
|
+
blockLogoUrl: this.current.blockLogoUrl,
|
|
544
751
|
};
|
|
545
|
-
this.http.post<any>('/
|
|
752
|
+
this.http.post<any>('/geo-block/admin/save', body).subscribe({
|
|
546
753
|
next: () => {
|
|
547
754
|
this.saving = false;
|
|
548
755
|
this.dirty = false;
|
|
549
|
-
this.notify.success('
|
|
756
|
+
this.notify.success('Site access settings saved');
|
|
757
|
+
this.reload();
|
|
550
758
|
},
|
|
551
759
|
error: (err) => {
|
|
552
760
|
this.saving = false;
|
|
@@ -554,20 +762,28 @@ export class GeoBlockComponent implements OnInit {
|
|
|
554
762
|
},
|
|
555
763
|
});
|
|
556
764
|
}
|
|
557
|
-
}
|
|
558
765
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
766
|
+
runSim() {
|
|
767
|
+
if (!this.current) return;
|
|
768
|
+
this.simBusy = true;
|
|
769
|
+
this.simResult = null;
|
|
770
|
+
this.http.post<any>('/geo-block/admin/simulate', {
|
|
771
|
+
token: this.current.token,
|
|
772
|
+
country: this.sim.country.trim().toUpperCase() || null,
|
|
773
|
+
region: this.sim.region.trim().toUpperCase() || null,
|
|
774
|
+
ip: this.sim.ip.trim() || null,
|
|
775
|
+
}).subscribe({
|
|
776
|
+
next: r => { this.simResult = r; this.simBusy = false; this.cdr.markForCheck(); },
|
|
777
|
+
error: () => { this.simBusy = false; this.notify.error('Simulation failed'); },
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
loadStats() {
|
|
782
|
+
if (!this.current) return;
|
|
783
|
+
if (this.stats) return; // load once on first visit
|
|
784
|
+
this.http.get<any>(`/geo-block/admin/stats?days=${this.statsDays}&channelId=${this.current.id}`).subscribe({
|
|
785
|
+
next: s => { this.stats = s; this.cdr.markForCheck(); },
|
|
786
|
+
error: () => this.notify.error('Failed to load stats'),
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|