@huloglobal/vendure-plugin-geo-block 0.1.0 โ 0.2.1
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/CHANGELOG.md +34 -0
- 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 +45 -25
- package/dist/geo-block.controller.d.ts.map +1 -1
- package/dist/geo-block.controller.js +390 -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 +27 -23
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +67 -38
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
- package/ui/components/geo-block.component.ts +519 -253
|
@@ -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,22 @@ 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="
|
|
42
|
+
<vdr-page-block *ngIf="updateBanner">
|
|
43
|
+
<div class="update-banner" [class.major]="updateBanner.isMajor">
|
|
44
|
+
<div>
|
|
45
|
+
<strong>๐ฆ Update available</strong>
|
|
46
|
+
{{ updateBanner.packageName }} {{ updateBanner.current }} โ <strong>{{ updateBanner.latest }}</strong>
|
|
47
|
+
<span *ngIf="updateBanner.isMajor" class="major-pill">major</span>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="actions">
|
|
50
|
+
<a [href]="'https://github.com/exceeded/vendure-plugin-geo-block/releases/tag/v' + updateBanner.latest" target="_blank" class="btn btn-sm btn-link">Release notes โ</a>
|
|
51
|
+
<button class="btn btn-sm" (click)="dismissUpdate()">Dismiss</button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</vdr-page-block>
|
|
55
|
+
|
|
56
|
+
<vdr-page-block *ngIf="!loading && current">
|
|
57
|
+
<div class="card top-bar">
|
|
37
58
|
<div class="card-block">
|
|
38
59
|
<div class="chan-row">
|
|
39
60
|
<label class="lbl">Channel</label>
|
|
@@ -44,172 +65,332 @@ interface CountryDef { value: string; label: string; flag: string; }
|
|
|
44
65
|
<span class="status-pill" [class.on]="current.enabled" [class.off]="!current.enabled">
|
|
45
66
|
{{ current.enabled ? 'GEO-BLOCK ON' : 'GEO-BLOCK OFF' }}
|
|
46
67
|
</span>
|
|
47
|
-
<button class="btn btn-sm btn-
|
|
68
|
+
<button class="btn btn-sm" [class.btn-warning]="current.enabled" [class.btn-primary]="!current.enabled" (click)="toggleEnabled()">
|
|
48
69
|
{{ current.enabled ? 'Turn off' : 'Turn on' }}
|
|
49
70
|
</button>
|
|
71
|
+
|
|
72
|
+
<span class="mode-pill" *ngIf="current.enabled" [class.mode-block]="current.mode === 'block'" [class.mode-soft]="current.mode === 'soft'">
|
|
73
|
+
{{ current.mode === 'soft' ? 'Soft block (banner)' : 'Full block' }}
|
|
74
|
+
</span>
|
|
75
|
+
|
|
76
|
+
<span class="dirty-flag" *ngIf="dirty">โ Unsaved</span>
|
|
50
77
|
</div>
|
|
51
78
|
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
79
|
+
<div class="tabs">
|
|
80
|
+
<button class="tab" [class.active]="tab === 'rules'" (click)="tab = 'rules'">Rules</button>
|
|
81
|
+
<button class="tab" [class.active]="tab === 'message'" (click)="tab = 'message'">Block page</button>
|
|
82
|
+
<button class="tab" [class.active]="tab === 'allowlist'" (click)="tab = 'allowlist'">IP allowlist</button>
|
|
83
|
+
<button class="tab" [class.active]="tab === 'simulate'" (click)="tab = 'simulate'">Simulate</button>
|
|
84
|
+
<button class="tab" [class.active]="tab === 'stats'" (click)="tab = 'stats'; loadStats()">Stats</button>
|
|
85
|
+
</div>
|
|
56
86
|
</div>
|
|
57
87
|
</div>
|
|
58
88
|
</vdr-page-block>
|
|
59
89
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
</
|
|
90
|
+
<!-- ============================================================= RULES TAB -->
|
|
91
|
+
<ng-container *ngIf="!loading && current && tab === 'rules'">
|
|
92
|
+
<vdr-page-block>
|
|
93
|
+
<div class="card">
|
|
94
|
+
<div class="card-block">
|
|
95
|
+
<h3 class="step-title">Mode</h3>
|
|
96
|
+
<div class="mode-grid">
|
|
97
|
+
<label class="mode-card" [class.active]="current.mode === 'block'">
|
|
98
|
+
<input type="radio" name="bmode" value="block" [(ngModel)]="current.mode" (ngModelChange)="markDirty()">
|
|
99
|
+
<div class="mode-title">Full block</div>
|
|
100
|
+
<div class="mode-body">Blocked visitors never see the storefront โ they get the block page (or are redirected).</div>
|
|
101
|
+
</label>
|
|
102
|
+
<label class="mode-card" [class.active]="current.mode === 'soft'">
|
|
103
|
+
<input type="radio" name="bmode" value="soft" [(ngModel)]="current.mode" (ngModelChange)="markDirty()">
|
|
104
|
+
<div class="mode-title">Soft block (browse-only)</div>
|
|
105
|
+
<div class="mode-body">Visitors can browse but a banner explains you don't ship to their country and checkout is hidden.</div>
|
|
106
|
+
</label>
|
|
107
|
+
</div>
|
|
78
108
|
</div>
|
|
79
109
|
</div>
|
|
80
|
-
</
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
110
|
+
</vdr-page-block>
|
|
111
|
+
|
|
112
|
+
<vdr-page-block>
|
|
113
|
+
<div class="card">
|
|
114
|
+
<div class="card-block">
|
|
115
|
+
<h3 class="step-title">Strategy</h3>
|
|
116
|
+
<div class="mode-grid">
|
|
117
|
+
<label class="mode-card" [class.active]="strategy === 'specific'">
|
|
118
|
+
<input type="radio" name="strat" value="specific" [(ngModel)]="strategy" (ngModelChange)="onStrategyChange()">
|
|
119
|
+
<div class="mode-title">Allow only specific places</div>
|
|
120
|
+
<div class="mode-body">Pick regions or individual countries โ everyone else is blocked.</div>
|
|
121
|
+
</label>
|
|
122
|
+
<label class="mode-card" [class.active]="strategy === 'worldwide'">
|
|
123
|
+
<input type="radio" name="strat" value="worldwide" [(ngModel)]="strategy" (ngModelChange)="onStrategyChange()">
|
|
124
|
+
<div class="mode-title">Worldwide except blocked</div>
|
|
125
|
+
<div class="mode-body">Allow everyone except the denylist below.</div>
|
|
126
|
+
</label>
|
|
127
|
+
</div>
|
|
95
128
|
</div>
|
|
96
129
|
</div>
|
|
97
|
-
</
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
130
|
+
</vdr-page-block>
|
|
131
|
+
|
|
132
|
+
<vdr-page-block *ngIf="strategy === 'specific'">
|
|
133
|
+
<div class="card">
|
|
134
|
+
<div class="card-block">
|
|
135
|
+
<h3 class="step-title">Allowed regions <small>({{ pickedRegionCount() }} picked)</small></h3>
|
|
136
|
+
<p class="hint">One-click presets. Tick as many as you want โ they stack.</p>
|
|
137
|
+
|
|
138
|
+
<input class="form-input filter-input" placeholder="Filter presetsโฆ" [(ngModel)]="presetFilter">
|
|
139
|
+
|
|
140
|
+
<div class="preset-section" *ngFor="let group of presetGroups">
|
|
141
|
+
<h4 class="group-title">{{ group.label }}</h4>
|
|
142
|
+
<div class="preset-grid">
|
|
143
|
+
<label *ngFor="let p of filteredPresets(group.kind)" class="preset-card" [class.active]="isRegionPicked(p.key)">
|
|
144
|
+
<input type="checkbox" [checked]="isRegionPicked(p.key)" (change)="toggleRegion(p.key)">
|
|
145
|
+
<div class="preset-label">{{ p.label }}</div>
|
|
146
|
+
<div class="preset-hint">{{ p.description }}<span *ngIf="p.countryCount"> ยท {{ p.countryCount }} countries</span></div>
|
|
147
|
+
</label>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
112
150
|
</div>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<
|
|
151
|
+
</div>
|
|
152
|
+
</vdr-page-block>
|
|
153
|
+
|
|
154
|
+
<vdr-page-block *ngIf="strategy === 'specific'">
|
|
155
|
+
<div class="card">
|
|
156
|
+
<div class="card-block">
|
|
157
|
+
<h3 class="step-title">Extra allowed countries <small>(optional)</small></h3>
|
|
158
|
+
<p class="hint">Add countries that aren't covered by a preset above.</p>
|
|
159
|
+
<div class="chip-row">
|
|
160
|
+
<span class="chip" *ngFor="let cc of current.extraAllowed">
|
|
161
|
+
{{ countryLabel(cc) }}
|
|
162
|
+
<button class="chip-x" (click)="removeExtra(cc)" title="Remove">ร</button>
|
|
163
|
+
</span>
|
|
164
|
+
<span *ngIf="!current.extraAllowed.length" class="hint inline">None yet.</span>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="picker">
|
|
167
|
+
<input class="form-input" placeholder="Country code (e.g. JP, IL, BR)" [(ngModel)]="newExtra" (keyup.enter)="addExtra()" maxlength="2" style="text-transform: uppercase">
|
|
168
|
+
<button class="btn btn-secondary btn-sm" (click)="addExtra()" [disabled]="!newExtra">+ Add</button>
|
|
169
|
+
</div>
|
|
120
170
|
</div>
|
|
121
171
|
</div>
|
|
122
|
-
</
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<
|
|
136
|
-
</
|
|
137
|
-
<
|
|
172
|
+
</vdr-page-block>
|
|
173
|
+
|
|
174
|
+
<vdr-page-block>
|
|
175
|
+
<div class="card">
|
|
176
|
+
<div class="card-block">
|
|
177
|
+
<h3 class="step-title">Always-blocked countries</h3>
|
|
178
|
+
<p class="hint" *ngIf="strategy === 'specific'">Subtracted from the allow-list โ e.g. block ๐ท๐บ while allowing “Europe”.</p>
|
|
179
|
+
<p class="hint" *ngIf="strategy === 'worldwide'">In worldwide mode this is the <em>only</em> filter โ everyone except these countries is allowed.</p>
|
|
180
|
+
<div class="chip-row">
|
|
181
|
+
<span class="chip blocked" *ngFor="let cc of current.blockedCountries">
|
|
182
|
+
{{ countryLabel(cc) }}
|
|
183
|
+
<button class="chip-x" (click)="removeBlocked(cc)" title="Remove">ร</button>
|
|
184
|
+
</span>
|
|
185
|
+
<span *ngIf="!current.blockedCountries.length" class="hint inline">None yet.</span>
|
|
186
|
+
</div>
|
|
187
|
+
<div class="picker">
|
|
188
|
+
<input class="form-input" placeholder="Country code (e.g. RU, IR)" [(ngModel)]="newBlocked" (keyup.enter)="addBlocked()" maxlength="2" style="text-transform: uppercase">
|
|
189
|
+
<button class="btn btn-secondary btn-sm" (click)="addBlocked()" [disabled]="!newBlocked">+ Add</button>
|
|
190
|
+
<span class="hint inline" style="margin-left: 12px">Common: RU, BY, IR, KP, SY, CU, MM</span>
|
|
191
|
+
</div>
|
|
138
192
|
</div>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
<
|
|
193
|
+
</div>
|
|
194
|
+
</vdr-page-block>
|
|
195
|
+
|
|
196
|
+
<vdr-page-block *ngIf="isUkResolved()">
|
|
197
|
+
<div class="card">
|
|
198
|
+
<div class="card-block">
|
|
199
|
+
<h3 class="step-title">UK subdivisions <small>(only applies when GB is allowed)</small></h3>
|
|
200
|
+
<p class="hint">Tighten to specific UK regions. Leave empty or pick all four to allow the whole UK.</p>
|
|
201
|
+
<div class="uk-row">
|
|
202
|
+
<label *ngFor="let r of ukRegions" class="uk-pill" [class.active]="current.allowedGbRegions.includes(r.value)">
|
|
203
|
+
<input type="checkbox" [checked]="current.allowedGbRegions.includes(r.value)" (change)="toggleUkRegion(r.value)">
|
|
204
|
+
{{ r.label }}
|
|
205
|
+
</label>
|
|
206
|
+
</div>
|
|
146
207
|
</div>
|
|
147
208
|
</div>
|
|
148
|
-
</
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
209
|
+
</vdr-page-block>
|
|
210
|
+
|
|
211
|
+
<vdr-page-block>
|
|
212
|
+
<div class="card preview-card">
|
|
213
|
+
<div class="card-block">
|
|
214
|
+
<h3 class="step-title">Resolved allow-list</h3>
|
|
215
|
+
<div *ngIf="!current.enabled" class="preview-banner preview-off">
|
|
216
|
+
<strong>Geo-block is OFF</strong> โ everyone can visit.
|
|
217
|
+
</div>
|
|
218
|
+
<div *ngIf="current.enabled">
|
|
219
|
+
<div class="preview-banner preview-allow">
|
|
220
|
+
<strong *ngIf="resolvedAllowed() === null">โ
Allow visitors from anywhere</strong>
|
|
221
|
+
<strong *ngIf="resolvedAllowed() !== null && resolvedAllowed()!.length">
|
|
222
|
+
โ
Allow visitors from {{ resolvedAllowed()!.length }} {{ resolvedAllowed()!.length === 1 ? 'country' : 'countries' }}
|
|
223
|
+
</strong>
|
|
224
|
+
<strong *ngIf="resolvedAllowed() !== null && !resolvedAllowed()!.length" class="warn">
|
|
225
|
+
โ ๏ธ Nothing is allowed โ every visitor will be blocked.
|
|
226
|
+
</strong>
|
|
227
|
+
<div class="country-chips" *ngIf="resolvedAllowed() !== null && resolvedAllowed()!.length">
|
|
228
|
+
<span class="mini-chip" *ngFor="let cc of resolvedAllowed()!">{{ cc }}</span>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
<div class="preview-banner preview-block" *ngIf="current.blockedCountries.length">
|
|
232
|
+
<strong>๐ซ Always block</strong>
|
|
233
|
+
<div class="country-chips">
|
|
234
|
+
<span class="mini-chip blocked" *ngFor="let cc of current.blockedCountries">{{ cc }}</span>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
164
238
|
</div>
|
|
165
239
|
</div>
|
|
166
|
-
</
|
|
167
|
-
</
|
|
240
|
+
</vdr-page-block>
|
|
241
|
+
</ng-container>
|
|
242
|
+
|
|
243
|
+
<!-- ============================================================= BLOCK PAGE TAB -->
|
|
244
|
+
<ng-container *ngIf="!loading && current && tab === 'message'">
|
|
245
|
+
<vdr-page-block>
|
|
246
|
+
<div class="card">
|
|
247
|
+
<div class="card-block">
|
|
248
|
+
<h3 class="step-title">Block page</h3>
|
|
249
|
+
<p class="hint">Customise what blocked visitors see. Leave blank for sensible defaults.</p>
|
|
250
|
+
|
|
251
|
+
<div class="form-row">
|
|
252
|
+
<label>Custom message <small>(optional)</small></label>
|
|
253
|
+
<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>
|
|
254
|
+
</div>
|
|
168
255
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
256
|
+
<div class="form-row">
|
|
257
|
+
<label>Redirect URL <small>(optional)</small></label>
|
|
258
|
+
<input class="form-input" [(ngModel)]="current.blockRedirectUrl" (ngModelChange)="markDirty()" placeholder="https://example.com/sorry">
|
|
259
|
+
<p class="hint">When set, blocked visitors are redirected here instead of seeing the block page.</p>
|
|
260
|
+
</div>
|
|
173
261
|
|
|
174
|
-
|
|
175
|
-
|
|
262
|
+
<div class="form-row">
|
|
263
|
+
<label>Logo URL <small>(optional)</small></label>
|
|
264
|
+
<input class="form-input" [(ngModel)]="current.blockLogoUrl" (ngModelChange)="markDirty()" placeholder="https://example.com/logo.svg">
|
|
265
|
+
</div>
|
|
176
266
|
</div>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
<
|
|
267
|
+
</div>
|
|
268
|
+
</vdr-page-block>
|
|
269
|
+
</ng-container>
|
|
270
|
+
|
|
271
|
+
<!-- ============================================================= IP ALLOWLIST TAB -->
|
|
272
|
+
<ng-container *ngIf="!loading && current && tab === 'allowlist'">
|
|
273
|
+
<vdr-page-block>
|
|
274
|
+
<div class="card">
|
|
275
|
+
<div class="card-block">
|
|
276
|
+
<h3 class="step-title">IP allowlist <small>(overrides every rule)</small></h3>
|
|
277
|
+
<p class="hint">IPs or IPv4 CIDR ranges that bypass all country / region rules. Use for your office, oncall engineers, payment processor probes.</p>
|
|
278
|
+
<div class="chip-row">
|
|
279
|
+
<span class="chip mono" *ngFor="let ip of current.ipAllowlist">
|
|
280
|
+
{{ ip }}
|
|
281
|
+
<button class="chip-x" (click)="removeIp(ip)" title="Remove">ร</button>
|
|
282
|
+
</span>
|
|
283
|
+
<span *ngIf="!current.ipAllowlist.length" class="hint inline">No bypass IPs configured.</span>
|
|
284
|
+
</div>
|
|
285
|
+
<div class="picker">
|
|
286
|
+
<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">
|
|
287
|
+
<button class="btn btn-secondary btn-sm" (click)="addIp()" [disabled]="!newIp">+ Add</button>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</vdr-page-block>
|
|
292
|
+
</ng-container>
|
|
293
|
+
|
|
294
|
+
<!-- ============================================================= SIMULATE TAB -->
|
|
295
|
+
<ng-container *ngIf="!loading && current && tab === 'simulate'">
|
|
296
|
+
<vdr-page-block>
|
|
297
|
+
<div class="card">
|
|
298
|
+
<div class="card-block">
|
|
299
|
+
<h3 class="step-title">Simulate a visitor</h3>
|
|
300
|
+
<p class="hint">Test exactly what your current rules will do for a hypothetical visitor โ without saving anything to the storefront.</p>
|
|
301
|
+
<div class="sim-grid">
|
|
302
|
+
<div>
|
|
303
|
+
<label>Country code</label>
|
|
304
|
+
<input class="form-input" [(ngModel)]="sim.country" placeholder="US" maxlength="2" style="text-transform: uppercase">
|
|
305
|
+
</div>
|
|
306
|
+
<div>
|
|
307
|
+
<label>UK region <small>(optional)</small></label>
|
|
308
|
+
<input class="form-input" [(ngModel)]="sim.region" placeholder="ENG / WLS / SCT / NIR" maxlength="3" style="text-transform: uppercase">
|
|
309
|
+
</div>
|
|
310
|
+
<div>
|
|
311
|
+
<label>IP address <small>(optional)</small></label>
|
|
312
|
+
<input class="form-input" [(ngModel)]="sim.ip" placeholder="203.0.113.42">
|
|
192
313
|
</div>
|
|
193
314
|
</div>
|
|
315
|
+
<button class="btn btn-primary" (click)="runSim()" [disabled]="simBusy">
|
|
316
|
+
{{ simBusy ? 'Runningโฆ' : 'Run simulation' }}
|
|
317
|
+
</button>
|
|
194
318
|
|
|
195
|
-
<div class="
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
319
|
+
<div class="sim-result" *ngIf="simResult">
|
|
320
|
+
<div *ngIf="simResult.ipMatchesAllowlist" class="sim-banner allow">
|
|
321
|
+
<strong>โ
Allowed</strong> โ IP matches the allowlist, every other rule is bypassed.
|
|
322
|
+
</div>
|
|
323
|
+
<div *ngIf="!simResult.ipMatchesAllowlist && simResult.verdict.allowed" class="sim-banner allow">
|
|
324
|
+
<strong>โ
Allowed</strong> ({{ simResult.verdict.reason }})
|
|
325
|
+
</div>
|
|
326
|
+
<div *ngIf="!simResult.ipMatchesAllowlist && !simResult.verdict.allowed" class="sim-banner deny">
|
|
327
|
+
<strong>๐ซ Blocked</strong> ({{ simResult.verdict.reason }})
|
|
199
328
|
</div>
|
|
200
329
|
</div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</vdr-page-block>
|
|
333
|
+
</ng-container>
|
|
334
|
+
|
|
335
|
+
<!-- ============================================================= STATS TAB -->
|
|
336
|
+
<ng-container *ngIf="!loading && current && tab === 'stats'">
|
|
337
|
+
<vdr-page-block>
|
|
338
|
+
<div class="card">
|
|
339
|
+
<div class="card-block">
|
|
340
|
+
<h3 class="step-title">Block statistics <small>last {{ statsDays }} days</small></h3>
|
|
341
|
+
|
|
342
|
+
<div *ngIf="!stats" class="hint">Loadingโฆ</div>
|
|
343
|
+
<div *ngIf="stats">
|
|
344
|
+
<div class="stats-grid">
|
|
345
|
+
<div class="stat-card">
|
|
346
|
+
<div class="num">{{ stats.totals.blocked || 0 }}</div>
|
|
347
|
+
<div class="lbl">Full blocks</div>
|
|
348
|
+
</div>
|
|
349
|
+
<div class="stat-card">
|
|
350
|
+
<div class="num">{{ stats.totals.softBlocked || 0 }}</div>
|
|
351
|
+
<div class="lbl">Soft blocks</div>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="stat-card">
|
|
354
|
+
<div class="num">{{ stats.totals.total || 0 }}</div>
|
|
355
|
+
<div class="lbl">Total events</div>
|
|
356
|
+
</div>
|
|
357
|
+
<div class="stat-card">
|
|
358
|
+
<div class="num">{{ stats.totals.uniqueIps || 0 }}</div>
|
|
359
|
+
<div class="lbl">Unique IPs</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
201
362
|
|
|
202
|
-
|
|
203
|
-
<
|
|
204
|
-
|
|
363
|
+
<h4 style="margin-top: 24px">Top blocked countries</h4>
|
|
364
|
+
<table class="table table-compact" *ngIf="stats.topCountries?.length">
|
|
365
|
+
<thead><tr><th>Country</th><th style="width: 100px">Blocked</th></tr></thead>
|
|
366
|
+
<tbody>
|
|
367
|
+
<tr *ngFor="let r of stats.topCountries">
|
|
368
|
+
<td>{{ r.country || 'โ' }}</td>
|
|
369
|
+
<td>{{ r.n }}</td>
|
|
370
|
+
</tr>
|
|
371
|
+
</tbody>
|
|
372
|
+
</table>
|
|
373
|
+
<p *ngIf="!stats.topCountries?.length" class="hint">No blocks recorded yet.</p>
|
|
374
|
+
|
|
375
|
+
<h4 style="margin-top: 24px">By reason</h4>
|
|
376
|
+
<table class="table table-compact" *ngIf="stats.reasons?.length">
|
|
377
|
+
<thead><tr><th>Reason</th><th style="width: 100px">Count</th></tr></thead>
|
|
378
|
+
<tbody>
|
|
379
|
+
<tr *ngFor="let r of stats.reasons">
|
|
380
|
+
<td>{{ r.reason }}</td>
|
|
381
|
+
<td>{{ r.n }}</td>
|
|
382
|
+
</tr>
|
|
383
|
+
</tbody>
|
|
384
|
+
</table>
|
|
205
385
|
</div>
|
|
206
386
|
</div>
|
|
207
387
|
</div>
|
|
208
|
-
</
|
|
209
|
-
</
|
|
388
|
+
</vdr-page-block>
|
|
389
|
+
</ng-container>
|
|
210
390
|
|
|
211
|
-
|
|
212
|
-
|
|
391
|
+
<!-- ============================================================= SAVE BAR -->
|
|
392
|
+
<vdr-page-block *ngIf="!loading && current && tab !== 'simulate' && tab !== 'stats'">
|
|
393
|
+
<div class="save-bar">
|
|
213
394
|
<button class="btn btn-primary" (click)="save()" [disabled]="saving">
|
|
214
395
|
{{ saving ? 'Savingโฆ' : 'Save changes' }}
|
|
215
396
|
</button>
|
|
@@ -220,26 +401,55 @@ interface CountryDef { value: string; label: string; flag: string; }
|
|
|
220
401
|
`,
|
|
221
402
|
styles: [`
|
|
222
403
|
:host { color: var(--color-text-100, inherit); display: block; }
|
|
404
|
+
.subtitle { font-size: 13px; color: var(--color-component-color-300); margin: 2px 0 0; }
|
|
405
|
+
|
|
406
|
+
.top-bar { border-top: 3px solid var(--color-primary-500, #1d4ed8); }
|
|
223
407
|
.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:
|
|
408
|
+
.lbl { font-size: 12px; color: var(--color-component-color-300); }
|
|
409
|
+
.form-select, .form-input {
|
|
410
|
+
padding: 6px 10px; border-radius: 4px; min-width: 180px;
|
|
227
411
|
border: 1px solid var(--color-component-border-200);
|
|
228
412
|
background: var(--color-component-bg-100);
|
|
229
413
|
color: var(--color-text-100, inherit);
|
|
230
414
|
}
|
|
415
|
+
.form-input.mono { font-family: var(--clr-font-family-monospace, monospace); }
|
|
416
|
+
.filter-input { width: 100%; max-width: 360px; margin-bottom: 12px; }
|
|
417
|
+
textarea.form-input { font-family: inherit; min-height: 80px; width: 100%; max-width: 600px; }
|
|
418
|
+
.form-row { margin: 12px 0; }
|
|
419
|
+
.form-row label { display: block; font-weight: 600; font-size: 13px; margin-bottom: 4px; }
|
|
420
|
+
.form-row label small { color: var(--color-component-color-300); font-weight: 400; margin-left: 4px; }
|
|
421
|
+
|
|
231
422
|
.status-pill {
|
|
232
423
|
display: inline-block; padding: 3px 12px; border-radius: 12px;
|
|
233
424
|
font-size: 11px; font-weight: 700; letter-spacing: 0.5px;
|
|
234
425
|
}
|
|
235
426
|
.status-pill.on { background: #10b981; color: #fff; }
|
|
236
427
|
.status-pill.off { background: var(--color-component-bg-200); color: var(--color-component-color-300); }
|
|
428
|
+
.mode-pill {
|
|
429
|
+
display: inline-block; padding: 3px 10px; border-radius: 10px;
|
|
430
|
+
font-size: 11px; font-weight: 600;
|
|
431
|
+
}
|
|
432
|
+
.mode-pill.mode-block { background: #fee2e2; color: #991b1b; }
|
|
433
|
+
.mode-pill.mode-soft { background: #fef3c7; color: #92400e; }
|
|
434
|
+
.dirty-flag { color: #f59e0b; font-weight: 600; font-size: 12px; }
|
|
435
|
+
|
|
436
|
+
.tabs { display: flex; gap: 4px; margin-top: 16px; border-bottom: 1px solid var(--color-component-border-200); }
|
|
437
|
+
.tab {
|
|
438
|
+
padding: 8px 16px;
|
|
439
|
+
background: transparent; border: 0; border-bottom: 2px solid transparent;
|
|
440
|
+
font-size: 13px; font-weight: 500;
|
|
441
|
+
color: var(--color-component-color-300); cursor: pointer;
|
|
442
|
+
margin-bottom: -1px;
|
|
443
|
+
}
|
|
444
|
+
.tab:hover { color: var(--color-text-100); }
|
|
445
|
+
.tab.active { border-bottom-color: var(--color-primary-500, #1d4ed8); color: var(--color-text-100); font-weight: 600; }
|
|
237
446
|
|
|
238
447
|
.hint { font-size: 13px; color: var(--color-component-color-300); margin: 6px 0 12px; }
|
|
239
448
|
.hint.inline { display: inline; margin: 0; }
|
|
240
449
|
|
|
241
450
|
.step-title { font-size: 16px; font-weight: 600; margin-bottom: 4px; }
|
|
242
|
-
.step-title small { color: var(--color-component-color-300); font-weight: 400; }
|
|
451
|
+
.step-title small { color: var(--color-component-color-300); font-weight: 400; margin-left: 6px; }
|
|
452
|
+
.group-title { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: var(--color-component-color-300); margin: 14px 0 8px; }
|
|
243
453
|
|
|
244
454
|
.mode-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
|
|
245
455
|
.mode-card {
|
|
@@ -254,6 +464,7 @@ interface CountryDef { value: string; label: string; flag: string; }
|
|
|
254
464
|
.mode-card .mode-title { font-weight: 600; font-size: 14px; margin-bottom: 4px; }
|
|
255
465
|
.mode-card .mode-body { font-size: 12px; color: var(--color-component-color-300); line-height: 1.5; }
|
|
256
466
|
|
|
467
|
+
.preset-section { margin-top: 8px; }
|
|
257
468
|
.preset-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; }
|
|
258
469
|
.preset-card {
|
|
259
470
|
display: block; padding: 12px;
|
|
@@ -264,7 +475,7 @@ interface CountryDef { value: string; label: string; flag: string; }
|
|
|
264
475
|
.preset-card.active { border-color: var(--color-primary-500, #1d4ed8); background: var(--color-component-bg-200); }
|
|
265
476
|
.preset-card input { float: right; }
|
|
266
477
|
.preset-label { font-weight: 600; font-size: 13px; }
|
|
267
|
-
.preset-hint { font-size: 11px; color: var(--color-component-color-300); margin-top: 4px; }
|
|
478
|
+
.preset-hint { font-size: 11px; color: var(--color-component-color-300); margin-top: 4px; line-height: 1.4; }
|
|
268
479
|
|
|
269
480
|
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; min-height: 30px; }
|
|
270
481
|
.chip {
|
|
@@ -274,6 +485,7 @@ interface CountryDef { value: string; label: string; flag: string; }
|
|
|
274
485
|
border: 1px solid #93c5fd;
|
|
275
486
|
}
|
|
276
487
|
.chip.blocked { background: #fee2e2; color: #991b1b; border-color: #fca5a5; }
|
|
488
|
+
.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
489
|
.chip-x {
|
|
278
490
|
background: transparent; border: none; cursor: pointer; padding: 0 0 0 2px;
|
|
279
491
|
color: inherit; font-size: 16px; line-height: 1;
|
|
@@ -304,6 +516,35 @@ interface CountryDef { value: string; label: string; flag: string; }
|
|
|
304
516
|
font-family: var(--clr-font-family-monospace, monospace);
|
|
305
517
|
}
|
|
306
518
|
.mini-chip.blocked { background: #fee2e2; color: #991b1b; border-color: #fca5a5; }
|
|
519
|
+
|
|
520
|
+
.sim-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 12px 0 16px; max-width: 700px; }
|
|
521
|
+
.sim-result { margin-top: 16px; }
|
|
522
|
+
.sim-banner { padding: 12px 16px; border-radius: 6px; font-size: 14px; }
|
|
523
|
+
.sim-banner.allow { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
|
|
524
|
+
.sim-banner.deny { background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; }
|
|
525
|
+
|
|
526
|
+
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; }
|
|
527
|
+
.stat-card {
|
|
528
|
+
padding: 14px 18px;
|
|
529
|
+
border: 1px solid var(--color-component-border-200);
|
|
530
|
+
border-radius: 6px;
|
|
531
|
+
background: var(--color-component-bg-100);
|
|
532
|
+
}
|
|
533
|
+
.stat-card .num { font-size: 24px; font-weight: 700; line-height: 1.2; color: var(--color-primary-500, #1d4ed8); }
|
|
534
|
+
.stat-card .lbl { font-size: 11px; color: var(--color-component-color-300); margin-top: 2px; }
|
|
535
|
+
|
|
536
|
+
.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; }
|
|
537
|
+
|
|
538
|
+
.update-banner {
|
|
539
|
+
display: flex; gap: 12px; align-items: center; justify-content: space-between; flex-wrap: wrap;
|
|
540
|
+
padding: 12px 16px; border-radius: 8px;
|
|
541
|
+
background: #ecfeff; border: 1px solid #67e8f9;
|
|
542
|
+
color: #155e75; font-size: 13px;
|
|
543
|
+
}
|
|
544
|
+
.update-banner.major { background: #fef3c7; border-color: #fde68a; color: #92400e; }
|
|
545
|
+
.update-banner strong { font-weight: 700; }
|
|
546
|
+
.update-banner .major-pill { display: inline-block; margin-left: 6px; padding: 1px 8px; border-radius: 8px; background: #f59e0b; color: #fff; font-size: 10px; font-weight: 700; text-transform: uppercase; }
|
|
547
|
+
.update-banner .actions { display: flex; gap: 8px; align-items: center; }
|
|
307
548
|
`],
|
|
308
549
|
})
|
|
309
550
|
export class GeoBlockComponent implements OnInit {
|
|
@@ -312,61 +553,33 @@ export class GeoBlockComponent implements OnInit {
|
|
|
312
553
|
channels: ChannelRow[] = [];
|
|
313
554
|
currentToken = '';
|
|
314
555
|
current: ChannelRow | null = null;
|
|
315
|
-
/** UI mode derived from underlying fields. */
|
|
316
|
-
mode: 'specific' | 'worldwide' = 'specific';
|
|
317
556
|
dirty = false;
|
|
557
|
+
|
|
558
|
+
tab: 'rules' | 'message' | 'allowlist' | 'simulate' | 'stats' = 'rules';
|
|
559
|
+
strategy: 'specific' | 'worldwide' = 'specific';
|
|
560
|
+
presetFilter = '';
|
|
561
|
+
|
|
318
562
|
newExtra = '';
|
|
319
563
|
newBlocked = '';
|
|
564
|
+
newIp = '';
|
|
320
565
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
{ value: 'OCEANIA', label: '๐ Oceania', hint: 'Australia, New Zealand.', countries: 2 },
|
|
331
|
-
];
|
|
566
|
+
sim = { country: '', region: '', ip: '' };
|
|
567
|
+
simBusy = false;
|
|
568
|
+
simResult: any = null;
|
|
569
|
+
|
|
570
|
+
stats: any = null;
|
|
571
|
+
statsDays = 30;
|
|
572
|
+
|
|
573
|
+
updateBanner: { packageName: string; current: string; latest: string; isMajor: boolean } | null = null;
|
|
574
|
+
private dismissKey = 'huloglobal-geo-block-update-dismissed';
|
|
332
575
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
{
|
|
337
|
-
{
|
|
338
|
-
{
|
|
339
|
-
{
|
|
340
|
-
{ value: 'NL', label: 'Netherlands', flag: '๐ณ๐ฑ' },
|
|
341
|
-
{ value: 'BE', label: 'Belgium', flag: '๐ง๐ช' },
|
|
342
|
-
{ value: 'LU', label: 'Luxembourg', flag: '๐ฑ๐บ' },
|
|
343
|
-
{ value: 'ES', label: 'Spain', flag: '๐ช๐ธ' },
|
|
344
|
-
{ value: 'PT', label: 'Portugal', flag: '๐ต๐น' },
|
|
345
|
-
{ value: 'IT', label: 'Italy', flag: '๐ฎ๐น' },
|
|
346
|
-
{ value: 'AT', label: 'Austria', flag: '๐ฆ๐น' },
|
|
347
|
-
{ value: 'CH', label: 'Switzerland', flag: '๐จ๐ญ' },
|
|
348
|
-
{ value: 'DK', label: 'Denmark', flag: '๐ฉ๐ฐ' },
|
|
349
|
-
{ value: 'SE', label: 'Sweden', flag: '๐ธ๐ช' },
|
|
350
|
-
{ value: 'NO', label: 'Norway', flag: '๐ณ๐ด' },
|
|
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: '๐จ๐ณ' },
|
|
576
|
+
presets: PresetMeta[] = [];
|
|
577
|
+
presetGroups = [
|
|
578
|
+
{ kind: 'all', label: 'Everywhere' },
|
|
579
|
+
{ kind: 'geography', label: 'By geography' },
|
|
580
|
+
{ kind: 'trade', label: 'Trade blocs' },
|
|
581
|
+
{ kind: 'political', label: 'Political / economic groups' },
|
|
582
|
+
{ kind: 'language', label: 'Language / cultural' },
|
|
370
583
|
];
|
|
371
584
|
|
|
372
585
|
ukRegions = [
|
|
@@ -382,19 +595,54 @@ export class GeoBlockComponent implements OnInit {
|
|
|
382
595
|
private cdr: ChangeDetectorRef,
|
|
383
596
|
) {}
|
|
384
597
|
|
|
385
|
-
ngOnInit() {
|
|
598
|
+
ngOnInit() {
|
|
599
|
+
this.http.get<{ presets: PresetMeta[] }>('/geo-block/presets').subscribe({
|
|
600
|
+
next: r => { this.presets = r.presets || []; this.cdr.markForCheck(); },
|
|
601
|
+
error: () => { /* presets are nice-to-have, not required */ },
|
|
602
|
+
});
|
|
603
|
+
this.loadStatus();
|
|
604
|
+
this.reload();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
loadStatus() {
|
|
608
|
+
this.http.get<any>('/geo-block/status').subscribe({
|
|
609
|
+
next: (s) => {
|
|
610
|
+
const u = s?.update;
|
|
611
|
+
if (!u?.updateAvailable || !u.latest) return;
|
|
612
|
+
let dismissed = '';
|
|
613
|
+
try { dismissed = localStorage.getItem(this.dismissKey) || ''; } catch {}
|
|
614
|
+
if (dismissed === u.latest) return;
|
|
615
|
+
this.updateBanner = { packageName: u.packageName, current: u.current, latest: u.latest, isMajor: !!u.isMajor };
|
|
616
|
+
this.cdr.markForCheck();
|
|
617
|
+
},
|
|
618
|
+
error: () => { /* nice-to-have */ },
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
dismissUpdate() {
|
|
623
|
+
if (!this.updateBanner) return;
|
|
624
|
+
try { localStorage.setItem(this.dismissKey, this.updateBanner.latest); } catch {}
|
|
625
|
+
this.updateBanner = null;
|
|
626
|
+
}
|
|
386
627
|
|
|
387
628
|
reload() {
|
|
388
629
|
this.loading = true;
|
|
389
630
|
this.dirty = false;
|
|
390
|
-
this.http.get<{ channels: ChannelRow[] }>('/
|
|
631
|
+
this.http.get<{ channels: ChannelRow[] }>('/geo-block/admin/channels').subscribe({
|
|
391
632
|
next: (res) => {
|
|
392
|
-
this.channels = res.channels || []
|
|
633
|
+
this.channels = (res.channels || []).map(c => ({
|
|
634
|
+
...c,
|
|
635
|
+
mode: c.mode || 'block',
|
|
636
|
+
ipAllowlist: c.ipAllowlist || [],
|
|
637
|
+
blockMessage: c.blockMessage || '',
|
|
638
|
+
blockRedirectUrl: c.blockRedirectUrl || '',
|
|
639
|
+
blockLogoUrl: c.blockLogoUrl || '',
|
|
640
|
+
}));
|
|
393
641
|
if (!this.currentToken && this.channels.length) {
|
|
394
642
|
this.currentToken = this.channels[0].token;
|
|
395
643
|
}
|
|
396
644
|
this.current = this.channels.find(c => c.token === this.currentToken) || null;
|
|
397
|
-
this.
|
|
645
|
+
this.deriveStrategy();
|
|
398
646
|
this.loading = false;
|
|
399
647
|
this.cdr.markForCheck();
|
|
400
648
|
},
|
|
@@ -408,24 +656,24 @@ export class GeoBlockComponent implements OnInit {
|
|
|
408
656
|
onChannelChange() {
|
|
409
657
|
this.current = this.channels.find(c => c.token === this.currentToken) || null;
|
|
410
658
|
this.dirty = false;
|
|
411
|
-
this.
|
|
659
|
+
this.deriveStrategy();
|
|
660
|
+
this.stats = null;
|
|
661
|
+
this.simResult = null;
|
|
412
662
|
}
|
|
413
663
|
|
|
414
|
-
private
|
|
664
|
+
private deriveStrategy() {
|
|
415
665
|
if (!this.current) return;
|
|
416
|
-
this.
|
|
666
|
+
this.strategy = this.current.allowedRegions.includes('WORLDWIDE') ? 'worldwide' : 'specific';
|
|
417
667
|
}
|
|
418
668
|
|
|
419
|
-
|
|
669
|
+
onStrategyChange() {
|
|
420
670
|
if (!this.current) return;
|
|
421
|
-
if (this.
|
|
422
|
-
// Replace presets with just WORLDWIDE (the resolver short-circuits).
|
|
671
|
+
if (this.strategy === 'worldwide') {
|
|
423
672
|
this.current.allowedRegions = ['WORLDWIDE'];
|
|
424
673
|
this.current.extraAllowed = [];
|
|
425
674
|
} else {
|
|
426
675
|
this.current.allowedRegions = this.current.allowedRegions.filter(r => r !== 'WORLDWIDE');
|
|
427
676
|
if (!this.current.allowedRegions.length && !this.current.extraAllowed.length) {
|
|
428
|
-
// Sensible default when switching back.
|
|
429
677
|
this.current.allowedRegions = ['UK_ONLY'];
|
|
430
678
|
}
|
|
431
679
|
}
|
|
@@ -452,17 +700,22 @@ export class GeoBlockComponent implements OnInit {
|
|
|
452
700
|
this.markDirty();
|
|
453
701
|
}
|
|
454
702
|
|
|
455
|
-
|
|
456
|
-
return this.
|
|
703
|
+
pickedRegionCount(): number {
|
|
704
|
+
return this.current?.allowedRegions.length || 0;
|
|
457
705
|
}
|
|
458
|
-
|
|
459
|
-
|
|
706
|
+
|
|
707
|
+
filteredPresets(kind: string): PresetMeta[] {
|
|
708
|
+
const filter = this.presetFilter.trim().toLowerCase();
|
|
709
|
+
return this.presets
|
|
710
|
+
.filter(p => p.kind === kind)
|
|
711
|
+
.filter(p => !filter || p.label.toLowerCase().includes(filter) || p.description.toLowerCase().includes(filter));
|
|
460
712
|
}
|
|
461
713
|
|
|
462
714
|
addExtra() {
|
|
463
715
|
if (!this.current || !this.newExtra) return;
|
|
464
|
-
|
|
465
|
-
|
|
716
|
+
const cc = this.newExtra.trim().toUpperCase();
|
|
717
|
+
if (cc.length === 2 && !this.current.extraAllowed.includes(cc)) {
|
|
718
|
+
this.current.extraAllowed = [...this.current.extraAllowed, cc];
|
|
466
719
|
this.markDirty();
|
|
467
720
|
}
|
|
468
721
|
this.newExtra = '';
|
|
@@ -475,8 +728,9 @@ export class GeoBlockComponent implements OnInit {
|
|
|
475
728
|
|
|
476
729
|
addBlocked() {
|
|
477
730
|
if (!this.current || !this.newBlocked) return;
|
|
478
|
-
|
|
479
|
-
|
|
731
|
+
const cc = this.newBlocked.trim().toUpperCase();
|
|
732
|
+
if (cc.length === 2 && !this.current.blockedCountries.includes(cc)) {
|
|
733
|
+
this.current.blockedCountries = [...this.current.blockedCountries, cc];
|
|
480
734
|
this.markDirty();
|
|
481
735
|
}
|
|
482
736
|
this.newBlocked = '';
|
|
@@ -487,6 +741,21 @@ export class GeoBlockComponent implements OnInit {
|
|
|
487
741
|
this.markDirty();
|
|
488
742
|
}
|
|
489
743
|
|
|
744
|
+
addIp() {
|
|
745
|
+
if (!this.current || !this.newIp) return;
|
|
746
|
+
const ip = this.newIp.trim();
|
|
747
|
+
if (ip && !this.current.ipAllowlist.includes(ip)) {
|
|
748
|
+
this.current.ipAllowlist = [...this.current.ipAllowlist, ip];
|
|
749
|
+
this.markDirty();
|
|
750
|
+
}
|
|
751
|
+
this.newIp = '';
|
|
752
|
+
}
|
|
753
|
+
removeIp(ip: string) {
|
|
754
|
+
if (!this.current) return;
|
|
755
|
+
this.current.ipAllowlist = this.current.ipAllowlist.filter(i => i !== ip);
|
|
756
|
+
this.markDirty();
|
|
757
|
+
}
|
|
758
|
+
|
|
490
759
|
toggleUkRegion(r: string) {
|
|
491
760
|
if (!this.current) return;
|
|
492
761
|
if (this.current.allowedGbRegions.includes(r)) {
|
|
@@ -502,34 +771,17 @@ export class GeoBlockComponent implements OnInit {
|
|
|
502
771
|
return allowed === null || allowed.includes('GB');
|
|
503
772
|
}
|
|
504
773
|
|
|
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
|
-
}
|
|
774
|
+
countryLabel(cc: string): string { return cc; }
|
|
514
775
|
|
|
515
|
-
/**
|
|
516
|
-
*
|
|
776
|
+
/** Local preview โ uses the server-resolved allowed list when no
|
|
777
|
+
* rule changes are pending. Best-effort otherwise. */
|
|
517
778
|
resolvedAllowed(): string[] | null {
|
|
518
779
|
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();
|
|
780
|
+
if (this.current.allowedRegions.includes('WORLDWIDE')) return null;
|
|
781
|
+
return this.current.resolved?.allowedCountries ?? null;
|
|
530
782
|
}
|
|
531
783
|
|
|
532
|
-
|
|
784
|
+
markDirty() { this.dirty = true; }
|
|
533
785
|
|
|
534
786
|
save() {
|
|
535
787
|
if (!this.current) return;
|
|
@@ -537,16 +789,22 @@ export class GeoBlockComponent implements OnInit {
|
|
|
537
789
|
const body = {
|
|
538
790
|
token: this.current.token,
|
|
539
791
|
enabled: this.current.enabled,
|
|
792
|
+
mode: this.current.mode,
|
|
540
793
|
allowedRegions: this.current.allowedRegions,
|
|
541
794
|
extraAllowed: this.current.extraAllowed,
|
|
542
795
|
blockedCountries: this.current.blockedCountries,
|
|
543
796
|
allowedGbRegions: this.current.allowedGbRegions,
|
|
797
|
+
ipAllowlist: this.current.ipAllowlist,
|
|
798
|
+
blockMessage: this.current.blockMessage,
|
|
799
|
+
blockRedirectUrl: this.current.blockRedirectUrl,
|
|
800
|
+
blockLogoUrl: this.current.blockLogoUrl,
|
|
544
801
|
};
|
|
545
|
-
this.http.post<any>('/
|
|
802
|
+
this.http.post<any>('/geo-block/admin/save', body).subscribe({
|
|
546
803
|
next: () => {
|
|
547
804
|
this.saving = false;
|
|
548
805
|
this.dirty = false;
|
|
549
|
-
this.notify.success('
|
|
806
|
+
this.notify.success('Site access settings saved');
|
|
807
|
+
this.reload();
|
|
550
808
|
},
|
|
551
809
|
error: (err) => {
|
|
552
810
|
this.saving = false;
|
|
@@ -554,20 +812,28 @@ export class GeoBlockComponent implements OnInit {
|
|
|
554
812
|
},
|
|
555
813
|
});
|
|
556
814
|
}
|
|
557
|
-
}
|
|
558
815
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
816
|
+
runSim() {
|
|
817
|
+
if (!this.current) return;
|
|
818
|
+
this.simBusy = true;
|
|
819
|
+
this.simResult = null;
|
|
820
|
+
this.http.post<any>('/geo-block/admin/simulate', {
|
|
821
|
+
token: this.current.token,
|
|
822
|
+
country: this.sim.country.trim().toUpperCase() || null,
|
|
823
|
+
region: this.sim.region.trim().toUpperCase() || null,
|
|
824
|
+
ip: this.sim.ip.trim() || null,
|
|
825
|
+
}).subscribe({
|
|
826
|
+
next: r => { this.simResult = r; this.simBusy = false; this.cdr.markForCheck(); },
|
|
827
|
+
error: () => { this.simBusy = false; this.notify.error('Simulation failed'); },
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
loadStats() {
|
|
832
|
+
if (!this.current) return;
|
|
833
|
+
if (this.stats) return; // load once on first visit
|
|
834
|
+
this.http.get<any>(`/geo-block/admin/stats?days=${this.statsDays}&channelId=${this.current.id}`).subscribe({
|
|
835
|
+
next: s => { this.stats = s; this.cdr.markForCheck(); },
|
|
836
|
+
error: () => this.notify.error('Failed to load stats'),
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}
|