[UX] #577 simplify VPD product forms
This commit is contained in:
@@ -104,6 +104,16 @@ UX-1부터 시작한다. 기존 데이터 모델과 VPD 로직은 바꾸지 않
|
||||
| 별도 Filter 관리 | 고급 접근 조건 | 예외 정책 기능 |
|
||||
| 운영 상태 / 연결 설정 | 운영 현황 / 시스템 설정 | 운영성·연동 설정 기능 |
|
||||
|
||||
상단 메뉴는 DDS와 같은 네 업무 영역인 `권한 관리 · 보호 정책 관리 · 지식자료 관리 · 접근 검증`으로 구성한다. ORDS·MCP·Filter·시스템 설정은 기본 제품 메뉴에서 빼고 관련 화면의 고급 링크에서만 연다. 첫 줄에는 제품명·1차 메뉴·로그아웃을 함께 둔다. 1차 메뉴를 누르면 하위 메뉴가 팝업으로 뜨지 않고 바로 아래 전폭 줄에 표시된다. 현재 페이지가 속한 메뉴는 처음부터 아래 줄을 열어 둔다. 절차나 구현 방식이 아니라 사용자가 하려는 일을 메뉴명으로 쓴다. 메뉴와 기본 화면에는 `1.`, `2.`, 단계 레일, `01/02` 배지를 두지 않는다. 순서가 실제 입력 제약인 wizard에서만 단계 번호를 사용한다.
|
||||
상단 메뉴는 DDS와 같은 네 업무 영역인 `권한 관리 · 보호 정책 관리 · 지식자료 관리 · 접근 검증`으로 구성한다. ORDS·MCP·Filter·시스템 설정은 기본 제품 메뉴에서 빼고 관련 화면의 고급 링크에서만 연다. 첫 줄에는 제품명·1차 메뉴·로그아웃을 함께 둔다. 1차 메뉴를 누르면 하위 메뉴가 팝업으로 뜨지 않고 바로 아래 전폭 줄에 표시된다. 현재 페이지가 속한 메뉴는 처음부터 아래 줄을 열어 둔다. 절차나 구현 방식이 아니라 사용자가 하려는 일을 메뉴명으로 쓴다. 메뉴와 기본 화면, wizard에도 `1.`, `2.`, 단계 레일, `01/02` 배지를 두지 않는다. 선택된 입력 영역은 역할·대상·접근 범위처럼 의미 있는 이름으로만 표시한다.
|
||||
|
||||
기본 페이지는 제목과 현재 설정·주요 행동만 노출한다. VPD/ORDS 처리 원리, 배경 설명, 주의사항은 상단의 공통 `도움말` 버튼을 눌렀을 때만 펼친다. 이 규칙으로 제품 화면이 관리 매뉴얼처럼 보이는 문제를 줄이고, 설정 자체를 전면에 둔다.
|
||||
|
||||
## 9. 화면 캡처 기반 정리 — 2026-07-01
|
||||
|
||||
로그인된 브라우저로 홈, 권한 주체·규칙·현황, 보호 상태·대상, 검증 세션·접근 검증·운영 현황, 지식자료·대화형 검색·검색 해석, 고급·설정 화면을 데스크톱 기준으로 캡처해 확인했다. MCP SSE와 Java client 화면은 데이터 조회 대기 때문에 브라우저 캡처를 완료하지 못했으며, 동일 템플릿·공통 레이아웃과 정적 화면 구조를 별도로 점검했다.
|
||||
|
||||
- 1차 메뉴는 첫 줄, 현재 업무 영역의 하위 메뉴는 둘째 줄에 나타나는 구조를 실제 사용자 관리 화면에서 확인했다.
|
||||
- 기본 화면에 남아 있던 단계 번호를 역할·대상·접근 범위 같은 작업명으로 바꾸고, 검증 세션·접근 검증·연동 점검에서도 순번을 제거한다.
|
||||
- 접근 규칙 목록은 ID·고정 ACTION·삭제 경고를 기본 열에서 빼고, 필요한 삭제 영향만 행의 `관리` 상세에서 확인하게 한다.
|
||||
- 보호 상태의 동작 설명은 상단 도움말로 옮기고, 기본 화면은 보호 대상과 적용 작업부터 보이게 한다.
|
||||
- 운영 현황은 긴 ORDS·VPD 값 때문에 오른쪽 열이 밀리지 않도록 고정 열 폭과 줄바꿈을 적용한다.
|
||||
|
||||
@@ -647,7 +647,6 @@ body {
|
||||
.wizard-panel-heading {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
gap: .75rem;
|
||||
}
|
||||
|
||||
.wizard-panel-heading h3 {
|
||||
@@ -1369,6 +1368,55 @@ body {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.permission-list-table {
|
||||
min-width: 760px;
|
||||
}
|
||||
|
||||
.permission-list-table th:nth-child(1) { width: 15%; }
|
||||
.permission-list-table th:nth-child(2) { width: 19%; }
|
||||
.permission-list-table th:nth-child(3) { width: 10%; }
|
||||
.permission-list-table th:nth-child(4) { width: 24%; }
|
||||
.permission-list-table th:nth-child(5) { width: 16%; }
|
||||
.permission-list-table th:nth-child(6) { width: 16%; }
|
||||
|
||||
.row-management {
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
.row-management > summary {
|
||||
color: var(--rw-primary);
|
||||
cursor: pointer;
|
||||
font-size: .84rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.row-management[open] {
|
||||
background: var(--rw-surface-muted);
|
||||
border: 1px solid var(--rw-border);
|
||||
border-radius: 8px;
|
||||
padding: .65rem;
|
||||
}
|
||||
|
||||
.operation-status-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.operation-status-table th,
|
||||
.operation-status-table td,
|
||||
.operation-status-table code {
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.operation-status-table th:nth-child(1) { width: 5%; }
|
||||
.operation-status-table th:nth-child(2) { width: 22%; }
|
||||
.operation-status-table th:nth-child(3) { width: 29%; }
|
||||
.operation-status-table th:nth-child(4) { width: 20%; }
|
||||
.operation-status-table th:nth-child(5) { width: 10%; }
|
||||
.operation-status-table th:nth-child(6) { width: 9%; }
|
||||
.operation-status-table th:nth-child(7) { width: 5%; }
|
||||
|
||||
.schema-summary {
|
||||
display: grid;
|
||||
gap: .75rem;
|
||||
|
||||
@@ -36,17 +36,17 @@
|
||||
</div>
|
||||
<div class="mcp-service-grid">
|
||||
<div class="mcp-service-item">
|
||||
<span>1</span>
|
||||
<span>연결 시작</span>
|
||||
<strong><code>initialize</code></strong>
|
||||
<small>기본 MCP serverInfo와 tools capability를 확인합니다.</small>
|
||||
</div>
|
||||
<div class="mcp-service-item">
|
||||
<span>2</span>
|
||||
<span>도구 목록</span>
|
||||
<strong><code>tools/list</code></strong>
|
||||
<small>현재 보호 객체에서 생성된 ORDS query tool과 설명/schema를 조회합니다.</small>
|
||||
</div>
|
||||
<div class="mcp-service-item">
|
||||
<span>3</span>
|
||||
<span>도구 호출</span>
|
||||
<strong><code>tools/call</code></strong>
|
||||
<small>tool 선택은 reasoning 이후 MCP client가 수행하므로 이 화면에서는 호출하지 않습니다.</small>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<section class="content-band">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<table class="table table-sm align-middle operation-status-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Health</th>
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
<p class="section-description">저장 전 적용 대상과 행 범위를 검토한 뒤 VPD 권한을 반영합니다.</p>
|
||||
</div>
|
||||
<div class="wizard-progress" aria-label="권한 추가 단계">
|
||||
<button class="wizard-step-indicator active" type="button" data-wizard-target="1">1 역할</button>
|
||||
<button class="wizard-step-indicator" type="button" data-wizard-target="2">2 객체</button>
|
||||
<button class="wizard-step-indicator" type="button" data-wizard-target="3">3 행 규칙</button>
|
||||
<button class="wizard-step-indicator" type="button" data-wizard-target="4">4 컬럼</button>
|
||||
<button class="wizard-step-indicator" type="button" data-wizard-target="5">5 검토</button>
|
||||
<button class="wizard-step-indicator active" type="button" data-wizard-target="1">역할</button>
|
||||
<button class="wizard-step-indicator" type="button" data-wizard-target="2">대상</button>
|
||||
<button class="wizard-step-indicator" type="button" data-wizard-target="3">접근 범위</button>
|
||||
<button class="wizard-step-indicator" type="button" data-wizard-target="4">표시 예외</button>
|
||||
<button class="wizard-step-indicator" type="button" data-wizard-target="5">검토</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="/permissions" class="permission-wizard" data-permission-wizard>
|
||||
@@ -61,7 +61,6 @@
|
||||
|
||||
<div class="wizard-panel active" data-wizard-step="1">
|
||||
<div class="wizard-panel-heading">
|
||||
<span class="wizard-step-number">1</span>
|
||||
<div>
|
||||
<h3>역할 선택</h3>
|
||||
<p>권한을 부여하거나 거부할 역할을 선택합니다.</p>
|
||||
@@ -96,7 +95,6 @@
|
||||
|
||||
<div class="wizard-panel" data-wizard-step="2">
|
||||
<div class="wizard-panel-heading">
|
||||
<span class="wizard-step-number">2</span>
|
||||
<div>
|
||||
<h3>보호 객체 선택</h3>
|
||||
<p>등록된 보호 객체 또는 DB 스키마 객체를 선택합니다.</p>
|
||||
@@ -138,7 +136,6 @@
|
||||
|
||||
<div class="wizard-panel" data-wizard-step="3">
|
||||
<div class="wizard-panel-heading">
|
||||
<span class="wizard-step-number">3</span>
|
||||
<div>
|
||||
<h3>권한 효과와 행 규칙</h3>
|
||||
<p>허용/거부 방향과 VPD 행 필터 조건을 설정합니다.</p>
|
||||
@@ -186,7 +183,6 @@
|
||||
|
||||
<div class="wizard-panel" data-wizard-step="4">
|
||||
<div class="wizard-panel-heading">
|
||||
<span class="wizard-step-number">4</span>
|
||||
<div>
|
||||
<h3>권한별 원문 표시 예외</h3>
|
||||
<p>이 역할이 이미 볼 수 있는 행에서 마스킹을 제외할 컬럼을 여러 개 선택합니다.</p>
|
||||
@@ -211,7 +207,6 @@
|
||||
|
||||
<div class="wizard-panel" data-wizard-step="5">
|
||||
<div class="wizard-panel-heading">
|
||||
<span class="wizard-step-number">5</span>
|
||||
<div>
|
||||
<h3>저장 전 검토</h3>
|
||||
<p>저장될 권한과 예상 effective policy를 확인합니다.</p>
|
||||
@@ -245,56 +240,52 @@
|
||||
<section class="content-band">
|
||||
<h2>접근 규칙 목록</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<table class="table table-sm align-middle permission-list-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>역할</th>
|
||||
<th>테이블/뷰</th>
|
||||
<th>Action</th>
|
||||
<th>Effect</th>
|
||||
<th>행 규칙</th>
|
||||
<th>적용 필터</th>
|
||||
<th>원문 표시 허용 컬럼(여러 개)</th>
|
||||
<th>삭제 영향</th>
|
||||
<th></th>
|
||||
<th>보호 대상</th>
|
||||
<th>효과</th>
|
||||
<th>접근 범위</th>
|
||||
<th>표시 예외</th>
|
||||
<th>관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="permission : ${permissions}"
|
||||
th:with="lastPermission=${lastPermissionByPermissionId[permission.permissionId()]}">
|
||||
<td th:text="${permission.permissionId()}">100</td>
|
||||
<td th:text="${permission.roleName()}">HR_DEPT_ROLE</td>
|
||||
<td th:text="${permission.objectName()}">CB_V_SEARCH_DOCUMENTS</td>
|
||||
<td th:text="${permission.action()}">SELECT</td>
|
||||
<td>
|
||||
<span class="badge"
|
||||
th:classappend="${permission.permissionEffect() == 'DENY'} ? ' text-bg-danger' : ' text-bg-success'"
|
||||
th:text="${permission.permissionEffect()}">ALLOW</span>
|
||||
</td>
|
||||
<td th:text="${permission.rules()} ?: '-'">ALL</td>
|
||||
<td><pre class="table-pre" th:text="${permission.filterPreview()} ?: '-'">ALL ROWS</pre></td>
|
||||
<td><pre class="table-pre" th:text="${permission.filterPreview()} ?: ${permission.rules()} ?: '-'">ALL ROWS</pre></td>
|
||||
<td th:text="${permission.visibleColumns()} ?: '-'">CONTENTS</td>
|
||||
<td class="delete-impact">
|
||||
<span>역할이 이 TABLE/VIEW의 SELECT 권한을 잃습니다.</span>
|
||||
<strong th:text="${permission.roleName() + ' -> ' + permission.objectName()}">ROLE -> OBJECT</strong>
|
||||
<small th:if="${lastPermission}" class="text-danger">이 객체의 마지막 권한입니다. 삭제 후 보호 객체가 비활성화됩니다.</small>
|
||||
<small th:unless="${lastPermission}">같은 객체에 다른 역할 권한이 남아 있습니다.</small>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="/permissions/delete" class="inline-form">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input type="hidden" name="permissionId" th:value="${permission.permissionId()}">
|
||||
<label class="delete-confirm">
|
||||
<input type="checkbox" name="confirmImpact" value="true" required>
|
||||
영향 확인
|
||||
</label>
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit">삭제</button>
|
||||
</form>
|
||||
<details class="row-management">
|
||||
<summary>관리</summary>
|
||||
<div class="delete-impact">
|
||||
<span>이 역할은 이 대상의 SELECT 권한을 잃습니다.</span>
|
||||
<strong th:text="${permission.roleName() + ' → ' + permission.objectName()}">ROLE → OBJECT</strong>
|
||||
<small th:if="${lastPermission}" class="text-danger">마지막 권한입니다. 삭제 후 보호 대상이 비활성화될 수 있습니다.</small>
|
||||
<small th:unless="${lastPermission}">같은 대상에 다른 역할 권한이 남아 있습니다.</small>
|
||||
</div>
|
||||
<form method="post" action="/permissions/delete" class="inline-form mt-2">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input type="hidden" name="permissionId" th:value="${permission.permissionId()}">
|
||||
<label class="delete-confirm">
|
||||
<input type="checkbox" name="confirmImpact" value="true" required>
|
||||
영향 확인
|
||||
</label>
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit">삭제</button>
|
||||
</form>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(permissions)}">
|
||||
<td colspan="10" class="text-muted">등록된 권한이 없습니다.</td>
|
||||
<td colspan="6" class="text-muted">등록된 접근 규칙이 없습니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<form hx-post="/probe" hx-target="#probe-result" hx-swap="innerHTML" class="form-grid probe-form">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<label class="span-2">
|
||||
1. 발급받은 토큰 원문
|
||||
발급받은 토큰 원문
|
||||
<input class="form-control" name="bearerToken" type="password" autocomplete="off"
|
||||
placeholder="토큰 발급 직후 복사한 값을 붙여 넣으세요">
|
||||
<span class="form-hint">원문은 DB에 저장되지 않습니다. 아래 검증 세션 사용자를 선택하면 이 입력은 무시되고 실행 중 발급·완료 즉시 폐기됩니다. 둘 다 비우면 invalid token 결과를 확인할 수 있습니다.</span>
|
||||
@@ -49,7 +49,7 @@
|
||||
<span class="form-hint">ORDS 호출에 사용할 10분 검증 세션을 만들고 완료 후 바로 폐기합니다.</span>
|
||||
</label>
|
||||
<label>
|
||||
2. 확인할 데이터
|
||||
확인할 데이터
|
||||
<select class="form-select" name="objectId" required>
|
||||
<option th:each="object : ${objects}"
|
||||
th:value="${object.objectId()}"
|
||||
@@ -77,18 +77,18 @@
|
||||
</select>
|
||||
<span class="form-hint">자료 등록 때 사용한 방식·차원과 맞춰야 합니다. AI 설정이 없으면 로컬 임베딩을 사용하세요.</span>
|
||||
</label>
|
||||
<button class="btn rw-btn-primary probe-submit" type="submit">3. 접근 검증 실행</button>
|
||||
<button class="btn rw-btn-primary probe-submit" type="submit">접근 검증 실행</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="probe-result" class="content-band" aria-live="polite">
|
||||
<div class="empty-result-guide">
|
||||
<strong>결과는 세 가지 순서로 설명합니다.</strong>
|
||||
<ol>
|
||||
<strong>검증 결과</strong>
|
||||
<ul>
|
||||
<li>토큰이 어떤 사용자와 역할로 해석됐는지</li>
|
||||
<li>VPD 적용 후 실제로 몇 행이 보였는지</li>
|
||||
<li>예상과 다를 때 어디를 확인해야 하는지</li>
|
||||
</ol>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<section class="content-band">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>1. 검증할 사용자 선택</h2>
|
||||
<h2>검증 사용자 선택</h2>
|
||||
<p class="section-subtitle">토큰에는 사용자의 직접 역할과 그룹 상속 역할이 연결됩니다. 권한 자체를 토큰에 복사하지 않으므로 이후 권한 변경도 동적으로 반영됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,7 +47,7 @@
|
||||
용도 메모
|
||||
<input class="form-control" name="description" maxlength="200" placeholder="예: HR 권한 확인">
|
||||
</label>
|
||||
<button class="btn rw-btn-primary" type="submit">2. 검증 세션 발급</button>
|
||||
<button class="btn rw-btn-primary" type="submit">검증 세션 발급</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<h1>보호 상태</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>권한 화면에서 만든 사용자·그룹·역할·행·열 규칙을 Oracle VPD가 실제 TABLE/VIEW에 적용하도록 연결합니다.</p>
|
||||
<p>권한 화면에서 만든 사용자·그룹·역할·행·열 규칙을 Oracle VPD가 실제 TABLE/VIEW에 적용하도록 연결합니다. Policy는 어느 객체의 어떤 SQL에 어떤 Filter function을 붙일지 정하고, 실제 허용 조건은 권한 규칙과 Filter가 계산합니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -22,32 +22,8 @@
|
||||
<section class="content-band">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="architecture-kicker">기본 동작</span>
|
||||
<h2>권한체계 자동 적용</h2>
|
||||
</div>
|
||||
<a class="btn btn-sm rw-btn-secondary" href="/permissions">권한 규칙 확인</a>
|
||||
</div>
|
||||
<div class="policy-apply-flow" aria-label="VPD 적용 계층">
|
||||
<span>사용자·그룹·역할</span>
|
||||
<strong>→</strong>
|
||||
<span>객체별 행·열 권한</span>
|
||||
<strong>→</strong>
|
||||
<span>동적 VPD</span>
|
||||
<strong>→</strong>
|
||||
<span>허용된 데이터만 반환</span>
|
||||
</div>
|
||||
<p class="text-muted mb-0"><code>CB_AGENT_DOC_VPD_FILTER</code>가 요청마다 권한체계를 읽습니다. 일반 변경은 <a href="/permissions">권한 규칙</a>에서 합니다.</p>
|
||||
<details class="explanation-details mt-3">
|
||||
<summary>Policy와 Filter의 역할 구분 보기</summary>
|
||||
<p>Policy는 “어느 객체의 어떤 SQL에 어떤 Filter function을 붙일지”를 정합니다. 행의 실제 허용/차단 조건은 권한 규칙과 Filter function이 계산하고, 컬럼 표시 보호는 별도 설정입니다.</p>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>보호할 수 있는 DB 객체</h2>
|
||||
<p class="section-subtitle">VPD 적용 상태와 권한·검증 경로를 확인합니다.</p>
|
||||
<h2>보호 대상</h2>
|
||||
<p class="section-subtitle">VPD 적용과 검증 준비 상태를 확인합니다.</p>
|
||||
<details class="explanation-details">
|
||||
<summary>객체 상태 항목 설명 보기</summary>
|
||||
<p>VPD가 붙었는지, 권한 규칙과 검증 경로가 준비됐는지 한곳에서 확인합니다. 아래 기본 적용에서는 객체만 선택하면 됩니다.</p>
|
||||
|
||||
@@ -59,6 +59,28 @@ class GuidedFlowTemplateTest {
|
||||
.doesNotContain("1 · CONTENT", "2 · ACCESS POLICY", "3 · SEARCH");
|
||||
}
|
||||
|
||||
@Test
|
||||
void productFormsUseTaskNamesInsteadOfVisibleStepNumbers() throws IOException {
|
||||
String permissions = template("permissions.html");
|
||||
String tokens = template("tokens.html");
|
||||
String probe = template("probe.html");
|
||||
String client = template("mcp-client-demo.html");
|
||||
|
||||
assertThat(permissions)
|
||||
.contains("data-wizard-target=\"1\">역할")
|
||||
.contains("data-wizard-target=\"2\">대상")
|
||||
.doesNotContain(">1 역할<", ">2 객체<", "wizard-step-number");
|
||||
assertThat(tokens)
|
||||
.contains("검증 사용자 선택", "검증 세션 발급")
|
||||
.doesNotContain("1. 검증할 사용자 선택", "2. 검증 세션 발급");
|
||||
assertThat(probe)
|
||||
.contains("접근 검증 실행", "<strong>검증 결과</strong>")
|
||||
.doesNotContain("1. 발급받은 토큰 원문", "2. 확인할 데이터", "3. 접근 검증 실행");
|
||||
assertThat(client)
|
||||
.contains("연결 시작", "도구 목록", "도구 호출")
|
||||
.doesNotContain("<span>1</span>", "<span>2</span>", "<span>3</span>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void probeUsesOneTokenInputAndHidesTechnicalExchangeByDefault() throws IOException {
|
||||
String probe = template("probe.html");
|
||||
|
||||
Reference in New Issue
Block a user