chore: start working on v2
|
@ -16,7 +16,7 @@ To start a Dev Keycloak instance that can show the pub.solar theme, you need to
|
|||
-e KEYCLOAK_ADMIN_PASSWORD=admin \
|
||||
-v $(pwd):/opt/keycloak/themes/pub.solar \
|
||||
-v $(pwd)/.dev-import:/opt/keycloak/data/import \
|
||||
quay.io/keycloak/keycloak:21.1.1 \
|
||||
quay.io/keycloak/keycloak:23.0.1 \
|
||||
start-dev --import-realm --features="declarative-user-profile"
|
||||
```
|
||||
|
||||
|
|
1
account/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
./resources
|
|
@ -1,65 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='account' bodyClass='user'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("editAccountHtmlTitle")}</h2>
|
||||
</div>
|
||||
<div class="col-md-2 subtitle">
|
||||
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="${url.accountUrl}" class="ps-container" method="post">
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
id="stateChecker"
|
||||
name="stateChecker"
|
||||
value="${stateChecker}"
|
||||
>
|
||||
|
||||
<#if !realm.registrationEmailAsUsername>
|
||||
<div class="ps-form-group ${messagesPerField.printIfExists('username','has-error')}">
|
||||
<label class="ps-form-group--label">${msg("username")}<#if realm.editUsernameAllowed> <span class="required">*</span></#if></label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="ps-input"
|
||||
id="username"
|
||||
name="username"
|
||||
<#if !realm.editUsernameAllowed>disabled="disabled"</#if> value="${(account.username!'')}"
|
||||
/>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<div class="ps-form-group ${messagesPerField.printIfExists('email','has-error')}">
|
||||
<label for="email" class="ps-form-group--label">${msg("email")} <span class="required">*</span></label>
|
||||
|
||||
<input type="text" class="ps-input" id="email" name="email" autofocus value="${(account.email!'')}"/>
|
||||
</div>
|
||||
|
||||
<div class="ps-form-group ${messagesPerField.printIfExists('firstName','has-error')}">
|
||||
<label for="firstName" class="ps-form-group--label">${msg("firstName")}</label>
|
||||
|
||||
<input type="text" class="ps-input" id="firstName" name="firstName" value="${(account.firstName!'')}"/>
|
||||
</div>
|
||||
|
||||
<div class="ps-form-group ${messagesPerField.printIfExists('lastName','has-error')}">
|
||||
<label for="lastName" class="ps-form-group--label">${msg("lastName")}</label>
|
||||
|
||||
<input type="text" class="ps-input" id="lastName" name="lastName" value="${(account.lastName!'')}"/>
|
||||
</div>
|
||||
|
||||
<div class="ps-form-group">
|
||||
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
|
||||
<div class="">
|
||||
<#if url.referrerURI??><a href="${url.referrerURI}">${kcSanitize(msg("backToApplication")?no_esc)}</a></#if>
|
||||
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Save">${msg("doSave")}</button>
|
||||
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Cancel">${msg("doCancel")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</@layout.mainLayout>
|
|
@ -1,85 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='applications' bodyClass='applications'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("applicationsHtmlTitle")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="spot-container" action="${url.applicationsUrl}" method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<input type="hidden" id="referrer" name="referrer" value="${stateChecker}">
|
||||
|
||||
<table class="ps-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>${msg("application")}</td>
|
||||
<td>${msg("availableRoles")}</td>
|
||||
<td>${msg("grantedPermissions")}</td>
|
||||
<td>${msg("additionalGrants")}</td>
|
||||
<td>${msg("action")}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<#list applications.applications as application>
|
||||
<tr>
|
||||
<td>
|
||||
<#if application.effectiveUrl?has_content>
|
||||
<a class="ps-link" href="${application.effectiveUrl}">
|
||||
</#if>
|
||||
<#if application.client.name?has_content>${advancedMsg(application.client.name)}<#else>${application.client.clientId}</#if>
|
||||
<#if application.effectiveUrl?has_content>
|
||||
</a>
|
||||
</#if>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<#list application.realmRolesAvailable as role>
|
||||
<#if role.description??>${advancedMsg(role.description)}<#else>${advancedMsg(role.name)}</#if>
|
||||
<#if role_has_next>, </#if>
|
||||
</#list>
|
||||
<#list application.resourceRolesAvailable?keys as resource>
|
||||
<#if application.realmRolesAvailable?has_content>, </#if>
|
||||
<#list application.resourceRolesAvailable[resource] as clientRole>
|
||||
<#if clientRole.roleDescription??>${advancedMsg(clientRole.roleDescription)}<#else>${advancedMsg(clientRole.roleName)}</#if>
|
||||
${msg("inResource")} <strong><#if clientRole.clientName??>${advancedMsg(clientRole.clientName)}<#else>${clientRole.clientId}</#if></strong>
|
||||
<#if clientRole_has_next>, </#if>
|
||||
</#list>
|
||||
</#list>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<#if application.client.consentRequired>
|
||||
<#list application.clientScopesGranted as claim>
|
||||
${advancedMsg(claim)}<#if claim_has_next>, </#if>
|
||||
</#list>
|
||||
<#else>
|
||||
<strong>${msg("fullAccess")}</strong>
|
||||
</#if>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<#list application.additionalGrants as grant>
|
||||
${advancedMsg(grant)}<#if grant_has_next>, </#if>
|
||||
</#list>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<#if (application.client.consentRequired && application.clientScopesGranted?has_content) || application.additionalGrants?has_content>
|
||||
<button
|
||||
class='${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!}'
|
||||
id='revoke-${application.client.clientId}'
|
||||
name='clientId'
|
||||
value="${application.client.id}"
|
||||
>${msg("revoke")}</button>
|
||||
</#if>
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
</@layout.mainLayout>
|
|
@ -1,42 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='social' bodyClass='social'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("federatedIdentitiesHtmlTitle")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="federated-identities">
|
||||
<#list federatedIdentity.identities as identity>
|
||||
<div class="row margin-bottom">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="${identity.providerId!}" class="control-label">${identity.displayName!}</label>
|
||||
</div>
|
||||
<div class="col-sm-5 col-md-5">
|
||||
<input disabled="true" class="form-control" value="${identity.userName!}">
|
||||
</div>
|
||||
<div class="col-sm-5 col-md-5">
|
||||
<#if identity.connected>
|
||||
<#if federatedIdentity.removeLinkPossible>
|
||||
<form action="${url.socialUrl}" method="post" class="form-inline">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<input type="hidden" id="action" name="action" value="remove">
|
||||
<input type="hidden" id="providerId" name="providerId" value="${identity.providerId!}">
|
||||
<button id="remove-link-${identity.providerId!}" class="btn btn-default">${msg("doRemove")}</button>
|
||||
</form>
|
||||
</#if>
|
||||
<#else>
|
||||
<form action="${url.socialUrl}" method="post" class="form-inline">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<input type="hidden" id="action" name="action" value="add">
|
||||
<input type="hidden" id="providerId" name="providerId" value="${identity.providerId!}">
|
||||
<button id="add-link-${identity.providerId!}" class="btn btn-default">${msg("doAdd")}</button>
|
||||
</form>
|
||||
</#if>
|
||||
</div>
|
||||
</div>
|
||||
</#list>
|
||||
</div>
|
||||
|
||||
</@layout.mainLayout>
|
25
account/frontend/.eslintrc.cjs
Normal file
|
@ -0,0 +1,25 @@
|
|||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript',
|
||||
'@vue/eslint-config-prettier/skip-formatting'
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}'
|
||||
],
|
||||
'extends': [
|
||||
'plugin:cypress/recommended'
|
||||
]
|
||||
}
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
}
|
||||
}
|
28
account/frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
8
account/frontend/.prettierrc.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
68
account/frontend/README.md
Normal file
|
@ -0,0 +1,68 @@
|
|||
# keycloak-theme-pub.solar
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||
|
||||
```sh
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Run End-to-End Tests with [Cypress](https://www.cypress.io/)
|
||||
|
||||
```sh
|
||||
npm run test:e2e:dev
|
||||
```
|
||||
|
||||
This runs the end-to-end tests against the Vite development server.
|
||||
It is much faster than the production build.
|
||||
|
||||
But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments):
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
8
account/frontend/cypress.config.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
|
||||
baseUrl: 'http://localhost:4173'
|
||||
}
|
||||
})
|
8
account/frontend/cypress/e2e/example.cy.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
// https://on.cypress.io/api
|
||||
|
||||
describe('My First Test', () => {
|
||||
it('visits the app root url', () => {
|
||||
cy.visit('/')
|
||||
cy.contains('h1', 'You did it!')
|
||||
})
|
||||
})
|
10
account/frontend/cypress/e2e/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["./**/*", "../support/**/*"],
|
||||
"compilerOptions": {
|
||||
"isolatedModules": false,
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress"]
|
||||
}
|
||||
}
|
5
account/frontend/cypress/fixtures/example.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
39
account/frontend/cypress/support/commands.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
export {}
|
20
account/frontend/cypress/support/e2e.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
24
account/frontend/env.d.ts
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
interface GlobalKeycloakData {
|
||||
developmentMode: boolean;
|
||||
authUrl: string;
|
||||
baseUrl: string;
|
||||
realm: string;
|
||||
resourceUrl: string;
|
||||
isReactLoading: boolean;
|
||||
|
||||
brandImg: string;
|
||||
brandUrl: string;
|
||||
|
||||
referrer: string;
|
||||
referrerName: string;
|
||||
referrerUri: string;
|
||||
|
||||
locale: string;
|
||||
l18nMsg: Record<string, string>;
|
||||
};
|
||||
|
||||
interface Window {
|
||||
globals: InitialData;
|
||||
}
|
13
account/frontend/index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
5927
account/frontend/package-lock.json
generated
Normal file
48
account/frontend/package.json
Normal file
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"name": "keycloak-theme-pub.solar",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest",
|
||||
"test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'",
|
||||
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"keycloak-js": "^23.0.1",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@tsconfig/node18": "^18.2.2",
|
||||
"@types/jsdom": "^21.1.3",
|
||||
"@types/node": "^18.18.5",
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/test-utils": "^2.4.1",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"cypress": "^13.3.1",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"npm-run-all2": "^6.1.1",
|
||||
"prettier": "^3.0.3",
|
||||
"sass": "^1.69.5",
|
||||
"start-server-and-test": "^2.0.1",
|
||||
"typescript": "~5.2.0",
|
||||
"vite": "^4.4.11",
|
||||
"vitest": "^0.34.6",
|
||||
"vue-tsc": "^1.8.19"
|
||||
}
|
||||
}
|
BIN
account/frontend/public/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
1
account/frontend/public/logo.svg
Normal file
After Width: | Height: | Size: 6.5 KiB |
12
account/frontend/src/App.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { RouterLink, RouterView } from 'vue-router';
|
||||
import Background from './components/Background';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
return () =>
|
||||
<>
|
||||
<RouterView />
|
||||
<Background />
|
||||
</>
|
||||
},
|
||||
};
|
86
account/frontend/src/assets/base.css
Normal file
|
@ -0,0 +1,86 @@
|
|||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
35
account/frontend/src/assets/main.css
Normal file
|
@ -0,0 +1,35 @@
|
|||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
35
account/frontend/src/components/Background.scss
Normal file
|
@ -0,0 +1,35 @@
|
|||
.ps-background {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
speak: none;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
|
||||
&--logo {
|
||||
width: 100px;
|
||||
margin: 0.1rem;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&--1312 {
|
||||
transform: rotate(34deg);
|
||||
transform-origin: center center;
|
||||
color: #eee;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
top: -50vh;
|
||||
left: -50vw;
|
||||
font-weight: 900;
|
||||
font-size: 7rem;
|
||||
line-height: 6rem;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
}
|
||||
}
|
45
account/frontend/src/components/Background.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import {
|
||||
ref,
|
||||
onBeforeUnmount,
|
||||
onBeforeMount,
|
||||
} from 'vue';
|
||||
|
||||
const x1312 = (new Array(400)).fill("0x1312").join(" ");
|
||||
const getLogoNum = () => (Math.ceil(window.innerWidth / 100) * Math.ceil(window.innerHeight / 100));
|
||||
|
||||
import './Background.scss';
|
||||
import './Logo.scss';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const count = ref(getLogoNum());
|
||||
const updateCount = () => setTimeout(() => {
|
||||
count.value = getLogoNum();
|
||||
}, 10);
|
||||
|
||||
onBeforeMount(() => {
|
||||
console.log('before mount');
|
||||
window.addEventListener('resize', updateCount);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
console.log('before unmount');
|
||||
window.removeEventListener('resize', updateCount);
|
||||
});
|
||||
|
||||
return () => <div class="ps-background">
|
||||
<div
|
||||
id="x1312"
|
||||
class="ps-background--1312"
|
||||
>{x1312}</div>
|
||||
{(new Array(count.value)).fill(null).map(() => (
|
||||
<div class="ps-background--logo ps-logo">
|
||||
<img
|
||||
class="ps-logo--base"
|
||||
src={`${window.globals.resourceUrl}/logo.svg`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>;
|
||||
},
|
||||
};
|
45
account/frontend/src/components/Logo.scss
Normal file
|
@ -0,0 +1,45 @@
|
|||
.ps-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100px;
|
||||
|
||||
&--base {
|
||||
animation-name: rotate;
|
||||
animation-duration: 0.3s;
|
||||
animation-timing-function: linear;
|
||||
animation-play-state: paused;
|
||||
animation-iteration-count: infinite;
|
||||
|
||||
&:hover {
|
||||
animation-play-state: running;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2n) &--base { animation-delay: -0.1s; }
|
||||
&:nth-child(3n) &--base { animation-delay: -0.3s; }
|
||||
&:nth-child(5n) &--base { animation-delay: -0.5s; }
|
||||
&:nth-child(7n) &--base { animation-delay: -7s; }
|
||||
&:nth-child(11n) &--base { animation-delay: -0.9s; }
|
||||
&:nth-child(13n) &--base { animation-delay: -1s; }
|
||||
|
||||
&--base::before {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
11
account/frontend/src/components/__tests__/HelloWorld.spec.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { mount } from '@vue/test-utils'
|
||||
import HelloWorld from '../HelloWorld.vue'
|
||||
|
||||
describe('HelloWorld', () => {
|
||||
it('renders properly', () => {
|
||||
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
|
||||
expect(wrapper.text()).toContain('Hello Vitest')
|
||||
})
|
||||
})
|
23
account/frontend/src/keycloak.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import Keycloak from 'keycloak-js';
|
||||
|
||||
export const keycloak = new Keycloak({
|
||||
url: window.location.origin,
|
||||
realm: window.globals.realm,
|
||||
clientId: window.globals.clientId,
|
||||
});
|
||||
|
||||
export const authenticated = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const authenticated = await keycloak.init({
|
||||
onLoad: 'check-sso',
|
||||
silentCheckSsoRedirectUri: `${location.origin}/silent-check-sso.html`,
|
||||
pkceMethod: 'S256',
|
||||
});
|
||||
console.log(`User is ${authenticated ? 'authenticated' : 'not authenticated'}`);
|
||||
|
||||
resolve(authenticated);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize adapter:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
17
account/frontend/src/main.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
import App from './App';
|
||||
import router from './router';
|
||||
import { keycloak, authenticated } from './keycloak';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
|
||||
app.mount('#app');
|
||||
|
||||
console.log(authenticated);
|
14
account/frontend/src/router/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: () => import('../views/Home.tsx'),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
12
account/frontend/src/stores/counter.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
8
account/frontend/src/views/Home.tsx
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default {
|
||||
setup() {
|
||||
return () => (
|
||||
<main>
|
||||
</main>
|
||||
);
|
||||
},
|
||||
};
|
12
account/frontend/tsconfig.app.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
14
account/frontend/tsconfig.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.vitest.json"
|
||||
}
|
||||
]
|
||||
}
|
16
account/frontend/tsconfig.node.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "@tsconfig/node18/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
9
account/frontend/tsconfig.vitest.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"lib": [],
|
||||
"types": ["node", "jsdom"]
|
||||
}
|
||||
}
|
32
account/frontend/vite.config.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { fileURLToPath, URL } from 'node:url';
|
||||
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
],
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
|
||||
build: {
|
||||
outDir: '../resources',
|
||||
assetsDir: 'assets',
|
||||
manifest: true,
|
||||
},
|
||||
|
||||
server: {
|
||||
proxy: {
|
||||
'/realms': 'http://localhost:8080',
|
||||
'/silent-check-sso.html': 'http://localhost:8080',
|
||||
},
|
||||
},
|
||||
});
|
14
account/frontend/vitest.config.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { fileURLToPath } from 'node:url'
|
||||
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
|
||||
import viteConfig from './vite.config'
|
||||
|
||||
export default mergeConfig(
|
||||
viteConfig,
|
||||
defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
exclude: [...configDefaults.exclude, 'e2e/*'],
|
||||
root: fileURLToPath(new URL('./', import.meta.url))
|
||||
}
|
||||
})
|
||||
)
|
73
account/index.ftl
Normal file
|
@ -0,0 +1,73 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>${msg("accountManagementTitle")}</title>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<script>
|
||||
window.globals = {
|
||||
<#if properties.developmentMode?has_content && properties.developmentMode == "true">
|
||||
developmentMode: true,
|
||||
<#else>
|
||||
developmentMode: false,
|
||||
</#if>
|
||||
clientId: '${clientId}',
|
||||
authUrl: '${authUrl}',
|
||||
baseUrl: '${baseUrl}',
|
||||
realm: '${realm.name}',
|
||||
resourceUrl: '${resourceUrl}',
|
||||
isReactLoading: false,
|
||||
|
||||
<#if properties.logoUrl?has_content>
|
||||
brandUrl: '${properties.logoUrl}',
|
||||
<#else>
|
||||
brandUrl: baseUrl,
|
||||
</#if>
|
||||
|
||||
<#if referrer??>
|
||||
referrer: '${referrer}',
|
||||
referrerName: '${referrerName}',
|
||||
referrerUri: '${referrer_uri}'.replace('&', '&'),
|
||||
</#if>
|
||||
|
||||
<#if msg??>
|
||||
locale: '${locale}',
|
||||
<#outputformat "JavaScript">
|
||||
l18nMsg: JSON.parse('${msgJSON?js_string}'),
|
||||
</#outputformat>
|
||||
<#else>
|
||||
locale: 'en',
|
||||
l18Msg: {},
|
||||
</#if>
|
||||
};
|
||||
|
||||
const features = {
|
||||
isRegistrationEmailAsUsername : ${realm.registrationEmailAsUsername?c},
|
||||
isEditUserNameAllowed : ${realm.editUsernameAllowed?c},
|
||||
isInternationalizationEnabled : ${realm.isInternationalizationEnabled()?c},
|
||||
isLinkedAccountsEnabled : ${realm.identityFederationEnabled?c},
|
||||
isMyResourcesEnabled : ${(realm.userManagedAccessAllowed && isAuthorizationEnabled)?c},
|
||||
deleteAccountAllowed : ${deleteAccountAllowed?c},
|
||||
updateEmailFeatureEnabled: ${updateEmailFeatureEnabled?c},
|
||||
updateEmailActionEnabled: ${updateEmailActionEnabled?c},
|
||||
isViewGroupsEnabled : ${isViewGroupsEnabled?c}
|
||||
};
|
||||
|
||||
const availableLocales = [];
|
||||
<#list supportedLocales as locale, label>
|
||||
availableLocales.push({locale : '${locale}', label : '${label}'});
|
||||
</#list>
|
||||
</script>
|
||||
|
||||
<script type="module" crossorigin src="${resourceUrl}/assets/index-f6484dd5.js"></script>
|
||||
<link rel="stylesheet" href="${resourceUrl}/assets/index-9c371cad.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,35 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='log' bodyClass='log'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("accountLogHtmlTitle")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>${msg("date")}</td>
|
||||
<td>${msg("event")}</td>
|
||||
<td>${msg("ip")}</td>
|
||||
<td>${msg("client")}</td>
|
||||
<td>${msg("details")}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<#list log.events as event>
|
||||
<tr>
|
||||
<td>${event.date?datetime}</td>
|
||||
<td>${event.event}</td>
|
||||
<td>${event.ipAddress}</td>
|
||||
<td>${event.client!}</td>
|
||||
<td><#list event.details as detail>${detail.key} = ${detail.value} <#if detail_has_next>, </#if></#list></td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</@layout.mainLayout>
|
|
@ -1,75 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='password' bodyClass='password'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("changePasswordHtmlTitle")}</h2>
|
||||
</div>
|
||||
<div class="col-md-2 subtitle">
|
||||
<span class="subtitle">${msg("allFieldsRequired")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="${url.passwordUrl}" class="ps-container" method="post">
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
class="ps-input"
|
||||
name="username"
|
||||
value="${(account.username!'')}"
|
||||
autocomplete="username"
|
||||
readonly="readonly"
|
||||
style="display:none;"
|
||||
>
|
||||
|
||||
<#if password.passwordSet>
|
||||
<div class="ps-form-group">
|
||||
<label for="password" class="ps-form-group--label">${msg("password")}</label>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
class="ps-input"
|
||||
id="password"
|
||||
name="password"
|
||||
autofocus
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
|
||||
<div class="ps-form-group">
|
||||
<label for="password-new" class="ps-form-group--label">${msg("passwordNew")}</label>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
class="ps-input"
|
||||
id="password-new"
|
||||
name="password-new"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ps-form-group">
|
||||
<label for="password-confirm" class="ps-form-group--label">${msg("passwordConfirm")}</label>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
class="ps-input"
|
||||
id="password-confirm"
|
||||
name="password-confirm"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ps-form-group">
|
||||
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
|
||||
<div class="">
|
||||
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Save">${msg("doSave")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</@layout.mainLayout>
|
|
@ -1,277 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='authorization' bodyClass='authorization'; section>
|
||||
|
||||
<style>
|
||||
.search-box,.close-icon,.search-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.search-wrapper {
|
||||
width: 500px;
|
||||
margin: auto;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.search-box {
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
border: 1px solid #006e9c;
|
||||
outline: 0;
|
||||
border-radius: 15px;
|
||||
background-color: #0085cf;
|
||||
padding: 2px 5px;
|
||||
|
||||
}
|
||||
.search-box:focus {
|
||||
box-shadow: 0 0 15px 5px #b0e0ee;
|
||||
border: 2px solid #bebede;
|
||||
}
|
||||
.close-icon {
|
||||
border:1px solid transparent;
|
||||
background-color: transparent;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.close-icon:after {
|
||||
display: block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-color: #FA9595;
|
||||
z-index:1;
|
||||
right: 35px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
padding: 2px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
box-shadow: 0 0 2px #E50F0F;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-box:not(:valid) ~ .close-icon {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function removeScopeElm(elm) {
|
||||
elm.parentNode.removeChild(elm);
|
||||
}
|
||||
|
||||
function removeAllScopes(id) {
|
||||
var scopesElm = document.getElementsByName('removeScope-' + id);
|
||||
|
||||
for (i = 0; i < scopesElm.length; i++) {
|
||||
var td = scopesElm[i].parentNode.parentNode;
|
||||
var tr = td.parentNode;
|
||||
var tbody = tr.parentNode;
|
||||
tbody.removeChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
function getChildren(parent, childId) {
|
||||
var childNodes = [];
|
||||
|
||||
for (i = 0; i < parent.childNodes.length; i++) {
|
||||
if (parent.childNodes[i].id == childId) {
|
||||
childNodes.push(parent.childNodes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return childNodes;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>
|
||||
<a href="${url.resourceUrl}">${msg("myResources")}</a> <i class="fa fa-angle-right"></i> <#if authorization.resource.displayName??>${authorization.resource.displayName}<#else>${authorization.resource.name}</#if>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<#if authorization.resource.iconUri??>
|
||||
<img src="${authorization.resource.iconUri}">
|
||||
<br/>
|
||||
</#if>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h3>
|
||||
${msg("peopleAccessResource")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${msg("user")}</th>
|
||||
<th>${msg("permission")}</th>
|
||||
<th>${msg("date")}</th>
|
||||
<th>${msg("action")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#if authorization.resource.shares?size != 0>
|
||||
<#list authorization.resource.shares as permission>
|
||||
<form action="${url.getResourceGrant(authorization.resource.id)}" name="revokeForm-${authorization.resource.id}-${permission.requester.username}" method="post">
|
||||
<input type="hidden" name="action" value="revoke">
|
||||
<input type="hidden" name="requester" value="${permission.requester.username}">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<tr>
|
||||
<td>
|
||||
<#if permission.requester.email??>${permission.requester.email}<#else>${permission.requester.username}</#if>
|
||||
</td>
|
||||
<td>
|
||||
<#if permission.scopes?size != 0>
|
||||
<#list permission.scopes as scope>
|
||||
<#if scope.granted && scope.scope??>
|
||||
<div class="search-box">
|
||||
<#if scope.scope.displayName??>
|
||||
${scope.scope.displayName}
|
||||
<#else>
|
||||
${scope.scope.name}
|
||||
</#if>
|
||||
<button class="close-icon" type="button" name="removeScope-${authorization.resource.id}-${permission.requester.username}" onclick="removeScopeElm(this.parentNode);document.forms['revokeForm-${authorization.resource.id}-${permission.requester.username}'].submit();"><i class="fa fa-times" aria-hidden="true"></i></button>
|
||||
<input type="hidden" name="permission_id" value="${scope.id}"/>
|
||||
</div>
|
||||
<#else>
|
||||
${msg("anyPermission")}
|
||||
</#if>
|
||||
</#list>
|
||||
<#else>
|
||||
Any action
|
||||
</#if>
|
||||
</td>
|
||||
<td>
|
||||
${permission.createdDate?datetime}
|
||||
</td>
|
||||
<td width="20%" align="middle" style="vertical-align: middle">
|
||||
<a href="#" id="revoke-${authorization.resource.name}-${permission.requester.username}" onclick="removeAllScopes('${authorization.resource.id}-${permission.requester.username}');document.forms['revokeForm-${authorization.resource.id}-${permission.requester.username}'].submit();" type="submit" class="btn btn-primary">${msg("doRevoke")}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
</#list>
|
||||
<#else>
|
||||
<tr>
|
||||
<td colspan="4">${msg("resourceIsNotBeingShared")}</td>
|
||||
</tr>
|
||||
</#if>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h3>
|
||||
${msg("resourceManagedPolicies")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${msg("description")}</th>
|
||||
<th>${msg("permission")}</th>
|
||||
<th>${msg("action")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#if authorization.resource.policies?size != 0>
|
||||
<#list authorization.resource.policies as permission>
|
||||
<form action="${url.getResourceGrant(authorization.resource.id)}" name="revokePolicyForm-${authorization.resource.id}-${permission.id}" method="post">
|
||||
<input type="hidden" name="action" value="revokePolicy">
|
||||
<input type="hidden" name="permission_id" value="${permission.id}"/>
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<tr>
|
||||
<td>
|
||||
<#if permission.description??>
|
||||
${permission.description}
|
||||
</#if>
|
||||
</td>
|
||||
<td>
|
||||
<#if permission.scopes?size != 0>
|
||||
<#list permission.scopes as scope>
|
||||
<div class="search-box">
|
||||
<#if scope.displayName??>
|
||||
${scope.displayName}
|
||||
<#else>
|
||||
${scope.name}
|
||||
</#if>
|
||||
<button class="close-icon" type="button" name="removePolicyScope-${authorization.resource.id}-${permission.id}-${scope.id}" onclick="removeScopeElm(this.parentNode);document.forms['revokePolicyForm-${authorization.resource.id}-${permission.id}'].submit();"><i class="fa fa-times" aria-hidden="true"></i></button>
|
||||
<input type="hidden" name="permission_id" value="${permission.id}:${scope.id}"/>
|
||||
</div>
|
||||
</#list>
|
||||
<#else>
|
||||
${msg("anyAction")}
|
||||
</#if>
|
||||
</td>
|
||||
<td width="20%" align="middle" style="vertical-align: middle">
|
||||
<a href="#" id="revokePolicy-${authorization.resource.name}-${permission.id}" onclick="document.forms['revokePolicyForm-${authorization.resource.id}-${permission.id}']['action'].value = 'revokePolicyAll';document.forms['revokePolicyForm-${authorization.resource.id}-${permission.id}'].submit();" type="submit" class="btn btn-primary">${msg("doRevoke")}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
</#list>
|
||||
<#else>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
${msg("resourceNoPermissionsGrantingAccess")}
|
||||
</td>
|
||||
</tr>
|
||||
</#if>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h3>
|
||||
${msg("shareWithOthers")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<form action="${url.getResourceShare(authorization.resource.id)}" name="shareForm" method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<div class="col-sm-3 col-md-3">
|
||||
<label for="password" class="control-label">${msg("username")} or ${msg("email")} </label> <span class="required">*</span>
|
||||
</div>
|
||||
<div class="col-sm-8 col-md-8">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<input type="text" class="form-control" id="user_id" name="user_id" autofocus autocomplete="off">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<br/>
|
||||
<#list authorization.resource.scopes as scope>
|
||||
<div id="scope" class="search-box">
|
||||
<#if scope.displayName??>
|
||||
${scope.displayName}
|
||||
<#else>
|
||||
${scope.name}
|
||||
</#if>
|
||||
<button class="close-icon" id="share-remove-scope-${authorization.resource.name}-${scope.name}" type="button" onclick="if (getChildren(this.parentNode.parentNode, 'scope').length > 1) {removeScopeElm(this.parentNode)}"><i class="fa fa-times" aria-hidden="true"></i></button>
|
||||
<input type="hidden" name="scope_id" value="${scope.id}"/>
|
||||
</div>
|
||||
</#list>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<br/>
|
||||
<a href="#" onclick="document.forms['shareForm'].submit()" type="submit" id="share-button" class="btn btn-primary">${msg("share")}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
</@layout.mainLayout>
|
|
@ -1,403 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='authorization' bodyClass='authorization'; section>
|
||||
<style>
|
||||
.search-box,.close-icon,.search-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.search-wrapper {
|
||||
width: 500px;
|
||||
margin: auto;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.search-box {
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
border: 1px solid #006e9c;
|
||||
outline: 0;
|
||||
border-radius: 15px;
|
||||
background-color: #0085cf;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
.search-box:focus {
|
||||
box-shadow: 0 0 15px 5px #b0e0ee;
|
||||
border: 2px solid #bebede;
|
||||
}
|
||||
.close-icon {
|
||||
border:1px solid transparent;
|
||||
background-color: transparent;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.close-icon:after {
|
||||
display: block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-color: #FA9595;
|
||||
z-index:1;
|
||||
right: 35px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
padding: 2px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
box-shadow: 0 0 2px #E50F0F;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-box:not(:valid) ~ .close-icon {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function showHideActions(elm) {
|
||||
if (elm.style.display == 'none') {
|
||||
elm.style.display = '';
|
||||
} else {
|
||||
elm.style.display = 'none';
|
||||
}
|
||||
}
|
||||
function removeScopeElm(elm) {
|
||||
var td = elm.parentNode;
|
||||
var tr = td.parentNode;
|
||||
var tbody = tr.parentNode;
|
||||
|
||||
td.removeChild(elm);
|
||||
|
||||
var childCount = td.childNodes.length - 1;
|
||||
|
||||
for (i = 0; i < td.childNodes.length; i++) {
|
||||
if (!td.childNodes[i].tagName || td.childNodes[i].tagName.toUpperCase() != 'DIV') {
|
||||
td.removeChild(td.childNodes[i]);
|
||||
childCount--;
|
||||
}
|
||||
}
|
||||
|
||||
if (childCount <= 0) {
|
||||
tbody.removeChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
function removeAllScopes(id) {
|
||||
var scopesElm = document.getElementsByName('removeScope-' + id);
|
||||
|
||||
for (i = 0; i < scopesElm.length; i++) {
|
||||
var td = scopesElm[i].parentNode.parentNode;
|
||||
var tr = td.parentNode;
|
||||
var tbody = tr.parentNode;
|
||||
tbody.removeChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
function selectAllCheckBoxes(formName, elm, name) {
|
||||
var shares = document.forms[formName].getElementsByTagName('input');
|
||||
|
||||
for (i = 0; i < shares.length; i++) {
|
||||
if (shares[i].name == name) {
|
||||
shares[i].checked = elm.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>
|
||||
${msg("myResources")}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<#if authorization.resourcesWaitingApproval?size != 0>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>
|
||||
${msg("needMyApproval")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${msg("resource")}</th>
|
||||
<th>${msg("requestor")}</th>
|
||||
<th>${msg("permissionRequestion")}</th>
|
||||
<th>${msg("action")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#list authorization.resourcesWaitingApproval as resource>
|
||||
<#list resource.permissions as permission>
|
||||
<form action="${url.getResourceGrant(resource.id)}" name="approveForm-${resource.id}-${permission.requester.username}" method="post">
|
||||
<input type="hidden" name="action" value="grant">
|
||||
<input type="hidden" name="requester" value="${permission.requester.username}">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<tr>
|
||||
<td>
|
||||
<#if resource.displayName??>${resource.displayName}<#else>${resource.name}</#if>
|
||||
</td>
|
||||
<td>
|
||||
<#if permission.requester.email??>${permission.requester.email}<#else>${permission.requester.username}</#if>
|
||||
</td>
|
||||
<td>
|
||||
<#list permission.scopes as scope>
|
||||
<#if scope.scope??>
|
||||
<div class="search-box">
|
||||
<#if scope.scope.displayName??>
|
||||
${scope.scope.displayName}
|
||||
<#else>
|
||||
${scope.scope.name}
|
||||
</#if>
|
||||
<button class="close-icon" type="button" id="grant-remove-scope-${resource.name}-${permission.requester.username}-${scope.scope.name}" name="removeScope-${resource.id}-${permission.requester.username}" onclick="removeScopeElm(this.parentNode);document.forms['approveForm-${resource.id}-${permission.requester.username}']['action'].value = 'deny';document.forms['approveForm-${resource.id}-${permission.requester.username}'].submit();"><i class="fa fa-times" aria-hidden="true"></i></button>
|
||||
<input type="hidden" name="permission_id" value="${scope.id}"/>
|
||||
</div>
|
||||
<#else>
|
||||
${msg("anyPermission")}
|
||||
</#if>
|
||||
</#list>
|
||||
</td>
|
||||
<td width="20%" align="middle" style="vertical-align: middle">
|
||||
<a href="#" id="grant-${resource.name}-${permission.requester.username}" onclick="document.forms['approveForm-${resource.id}-${permission.requester.username}']['action'].value = 'grant';document.forms['approveForm-${resource.id}-${permission.requester.username}'].submit();" type="submit" class="btn btn-primary">${msg("doApprove")}</a>
|
||||
<a href="#" id="deny-${resource.name}-${permission.requester.username}" onclick="removeAllScopes('${resource.id}-${permission.requester.username}');document.forms['approveForm-${resource.id}-${permission.requester.username}']['action'].value = 'deny';document.forms['approveForm-${resource.id}-${permission.requester.username}'].submit();" type="submit" class="btn btn-danger">${msg("doDeny")}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
</#list>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>
|
||||
${msg("myResourcesSub")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${msg("resource")}</th>
|
||||
<th>${msg("application")}</th>
|
||||
<th>${msg("peopleSharingThisResource")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<#if authorization.resources?size != 0>
|
||||
<#list authorization.resources as resource>
|
||||
<tr>
|
||||
<td>
|
||||
<a id="detail-${resource.name}" href="${url.getResourceDetailUrl(resource.id)}">
|
||||
<#if resource.displayName??>${resource.displayName}<#else>${resource.name}</#if>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<#if resource.resourceServer.baseUri??>
|
||||
<a href="${resource.resourceServer.baseUri}">${resource.resourceServer.name}</a>
|
||||
<#else>
|
||||
${resource.resourceServer.name}
|
||||
</#if>
|
||||
</td>
|
||||
<td>
|
||||
<#if resource.shares?size != 0>
|
||||
<a href="${url.getResourceDetailUrl(resource.id)}">${resource.shares?size} <i class="fa fa-users"></i></a>
|
||||
<#else>
|
||||
${msg("notBeingShared")}
|
||||
</#if>
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
<#else>
|
||||
<tr>
|
||||
<td colspan="4">${msg("notHaveAnyResource")}</td>
|
||||
</tr>
|
||||
</#if>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>
|
||||
${msg("resourcesSharedWithMe")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form action="${url.resourceUrl}" name="shareForm" method="post">
|
||||
<input type="hidden" name="action" value="cancel"/>
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%"><input type="checkbox" onclick="selectAllCheckBoxes('shareForm', this, 'resource_id');" <#if authorization.sharedResources?size == 0>disabled="true"</#if></td>
|
||||
<th>${msg("resource")}</th>
|
||||
<th>${msg("owner")}</th>
|
||||
<th>${msg("application")}</th>
|
||||
<th>${msg("permission")}</th>
|
||||
<th>${msg("date")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#if authorization.sharedResources?size != 0>
|
||||
<#list authorization.sharedResources as resource>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="resource_id" value="${resource.id}"/>
|
||||
</td>
|
||||
<td>
|
||||
<#if resource.displayName??>${resource.displayName}<#else>${resource.name}</#if>
|
||||
</td>
|
||||
<td>
|
||||
${resource.ownerName}
|
||||
</td>
|
||||
<td>
|
||||
<#if resource.resourceServer.baseUri??>
|
||||
<a href="${resource.resourceServer.baseUri}">${resource.resourceServer.name}</a>
|
||||
<#else>
|
||||
${resource.resourceServer.name}
|
||||
</#if>
|
||||
</td>
|
||||
<td>
|
||||
<#if resource.permissions?size != 0>
|
||||
<ul>
|
||||
<#list resource.permissions as permission>
|
||||
<#list permission.scopes as scope>
|
||||
<#if scope.granted && scope.scope??>
|
||||
<li>
|
||||
<#if scope.scope.displayName??>
|
||||
${scope.scope.displayName}
|
||||
<#else>
|
||||
${scope.scope.name}
|
||||
</#if>
|
||||
</li>
|
||||
<#else>
|
||||
${msg("anyPermission")}
|
||||
</#if>
|
||||
</#list>
|
||||
</#list>
|
||||
</ul>
|
||||
<#else>
|
||||
Any action
|
||||
</#if>
|
||||
</td>
|
||||
<td>
|
||||
${resource.permissions[0].grantedDate?datetime}
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
<#else>
|
||||
<tr>
|
||||
<td colspan="6">${msg("noResourcesSharedWithYou")}</td>
|
||||
</tr>
|
||||
</#if>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
<#if authorization.sharedResources?size != 0>
|
||||
<div class="col-md-12">
|
||||
<a href="#" onclick="document.forms['shareForm'].submit();" type="submit" class="btn btn-danger">${msg("doRemoveSharing")}</a>
|
||||
</div>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<#if authorization.resourcesWaitingOthersApproval?size != 0>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>
|
||||
${msg("requestsWaitingApproval")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<i class="pficon pficon-info"></i> ${msg("havePermissionRequestsWaitingForApproval",authorization.resourcesWaitingOthersApproval?size)}
|
||||
<a href="#" onclick="document.getElementById('waitingApproval').style.display=''">${msg("clickHereForDetails")}</a>
|
||||
<div class="row">
|
||||
<div class="col-md-12"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12"></div>
|
||||
</div>
|
||||
<div class="row" id="waitingApproval" style="display:none">
|
||||
<div class="col-md-12">
|
||||
<form action="${url.resourceUrl}" name="waitingApprovalForm" method="post">
|
||||
<input type="hidden" name="action" value="cancelRequest"/>
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%"><input type="checkbox" onclick="selectAllCheckBoxes('waitingApprovalForm', this, 'resource_id');" <#if authorization.resourcesWaitingOthersApproval?size == 0>disabled="true"</#if></th>
|
||||
<th>${msg("resource")}</th>
|
||||
<th>${msg("owner")}</th>
|
||||
<th>${msg("action")}</th>
|
||||
<th>${msg("date")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#list authorization.resourcesWaitingOthersApproval as resource>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="resource_id" value="${resource.id}"/>
|
||||
</td>
|
||||
<td>
|
||||
<#if resource.displayName??>${resource.displayName}<#else>${resource.name}</#if>
|
||||
</td>
|
||||
<td>
|
||||
${resource.ownerName}
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<#list resource.permissions as permission>
|
||||
<#list permission.scopes as scope>
|
||||
<li>
|
||||
<#if scope.scope??>
|
||||
<#if scope.scope.displayName??>
|
||||
${scope.scope.displayName}
|
||||
<#else>
|
||||
${scope.scope.name}
|
||||
</#if>
|
||||
<#else>
|
||||
${msg("anyPermission")}
|
||||
</#if>
|
||||
</li>
|
||||
</#list>
|
||||
</#list>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
${resource.permissions[0].createdDate?datetime}
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<a href="#" onclick="document.forms['waitingApprovalForm'].submit();" type="submit" class="btn btn-danger">${msg("doRemoveRequest")}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
</@layout.mainLayout>
|
1
account/resources/assets/Home-5ca1a495.js
Normal file
|
@ -0,0 +1 @@
|
|||
import{c as e}from"./index-f6484dd5.js";const o={setup(){return()=>e("main",null,null)}};export{o as default};
|
1
account/resources/assets/Home-7c18ddb3.js
Normal file
|
@ -0,0 +1 @@
|
|||
import{c as e}from"./index-1537da3e.js";const o={setup(){return()=>e("main",null,null)}};export{o as default};
|
21
account/resources/assets/index-1537da3e.js
Normal file
1
account/resources/assets/index-9c371cad.css
Normal file
|
@ -0,0 +1 @@
|
|||
:root{--vt-c-white: #ffffff;--vt-c-white-soft: #f8f8f8;--vt-c-white-mute: #f2f2f2;--vt-c-black: #181818;--vt-c-black-soft: #222222;--vt-c-black-mute: #282828;--vt-c-indigo: #2c3e50;--vt-c-divider-light-1: rgba(60, 60, 60, .29);--vt-c-divider-light-2: rgba(60, 60, 60, .12);--vt-c-divider-dark-1: rgba(84, 84, 84, .65);--vt-c-divider-dark-2: rgba(84, 84, 84, .48);--vt-c-text-light-1: var(--vt-c-indigo);--vt-c-text-light-2: rgba(60, 60, 60, .66);--vt-c-text-dark-1: var(--vt-c-white);--vt-c-text-dark-2: rgba(235, 235, 235, .64)}:root{--color-background: var(--vt-c-white);--color-background-soft: var(--vt-c-white-soft);--color-background-mute: var(--vt-c-white-mute);--color-border: var(--vt-c-divider-light-2);--color-border-hover: var(--vt-c-divider-light-1);--color-heading: var(--vt-c-text-light-1);--color-text: var(--vt-c-text-light-1);--section-gap: 160px}@media (prefers-color-scheme: dark){:root{--color-background: var(--vt-c-black);--color-background-soft: var(--vt-c-black-soft);--color-background-mute: var(--vt-c-black-mute);--color-border: var(--vt-c-divider-dark-2);--color-border-hover: var(--vt-c-divider-dark-1);--color-heading: var(--vt-c-text-dark-1);--color-text: var(--vt-c-text-dark-2)}}*,*:before,*:after{box-sizing:border-box;margin:0;font-weight:400}body{min-height:100vh;color:var(--color-text);background:var(--color-background);transition:color .5s,background-color .5s;line-height:1.6;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-size:15px;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#app{max-width:1280px;margin:0 auto;padding:2rem;font-weight:400}a,.green{text-decoration:none;color:#00bd7e;transition:.4s}@media (hover: hover){a:hover{background-color:#00bd7e33}}@media (min-width: 1024px){body{display:flex;place-items:center}#app{display:grid;grid-template-columns:1fr 1fr;padding:0 2rem}}.ps-background{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;speak:none;position:fixed;top:0;left:0;height:100vh;width:100vw;overflow:hidden}.ps-background--logo{width:100px;margin:.1rem;z-index:2}.ps-background--1312{transform:rotate(34deg);transform-origin:center center;color:#eee;position:fixed;z-index:1;top:-50vh;left:-50vw;font-weight:900;font-size:7rem;line-height:6rem;width:200vw;height:200vh}.ps-logo{display:flex;align-items:center;justify-content:center;position:relative;z-index:1;width:100px}.ps-logo--base{animation-name:rotate;animation-duration:.3s;animation-timing-function:linear;animation-play-state:paused;animation-iteration-count:infinite}.ps-logo--base:hover{animation-play-state:running}.ps-logo:nth-child(2n) .ps-logo--base{animation-delay:-.1s}.ps-logo:nth-child(3n) .ps-logo--base{animation-delay:-.3s}.ps-logo:nth-child(5n) .ps-logo--base{animation-delay:-.5s}.ps-logo:nth-child(7n) .ps-logo--base{animation-delay:-7s}.ps-logo:nth-child(11n) .ps-logo--base{animation-delay:-.9s}.ps-logo:nth-child(13n) .ps-logo--base{animation-delay:-1s}.ps-logo--base:before{position:absolute;display:block;content:"";width:100%;height:100%;border-radius:50%}@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}
|
21
account/resources/assets/index-f6484dd5.js
Normal file
|
@ -1,277 +0,0 @@
|
|||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #F9F9F9;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
header .navbar {
|
||||
margin-bottom: 0;
|
||||
min-height: inherit;
|
||||
}
|
||||
|
||||
.header .container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
background-image: url('../img/logo.png');
|
||||
height: 25px;
|
||||
background-repeat: no-repeat;
|
||||
width: 123px;
|
||||
margin: 3px 10px 5px;
|
||||
text-indent: -99999px;
|
||||
}
|
||||
|
||||
.navbar-pf .navbar-utility {
|
||||
right: 20px;
|
||||
top: -34px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.navbar-pf .navbar-utility > li > a {
|
||||
color: #fff !important;
|
||||
padding-bottom: 12px;
|
||||
padding-top: 11px;
|
||||
border-left: medium none;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
background-color: #fff;
|
||||
border-color: #CECECE;
|
||||
border-style: solid;
|
||||
border-width: 0 1px;
|
||||
height: 100%;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.margin-bottom {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
|
||||
.bs-sidebar {
|
||||
background-color: #f9f9f9;
|
||||
padding-top: 44px;
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
.bs-sidebar ul {
|
||||
list-style: none;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.bs-sidebar ul li {
|
||||
margin-bottom: 0.5em;
|
||||
margin-left: -1em;
|
||||
}
|
||||
.bs-sidebar ul li a {
|
||||
font-size: 14px;
|
||||
padding-left: 25px;
|
||||
color: #4d5258;
|
||||
line-height: 28px;
|
||||
display: block;
|
||||
border-width: 1px 0 1px 1px;
|
||||
border-style: solid;
|
||||
border-color: #f9f9f9;
|
||||
}
|
||||
.bs-sidebar ul li a:hover,
|
||||
.bs-sidebar ul li a:focus {
|
||||
text-decoration: none;
|
||||
color: #777777;
|
||||
border-right: 2px solid #aaa;
|
||||
}
|
||||
.bs-sidebar ul li.active a {
|
||||
background-color: #c7e5f0;
|
||||
border-color: #56bae0;
|
||||
font-weight: bold;
|
||||
background-image: url(../img/icon-sidebar-active.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: right center;
|
||||
}
|
||||
|
||||
.bs-sidebar ul li.active a:hover {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
|
||||
.content-area h2 {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-weight: 100;
|
||||
font-size: 24px;
|
||||
margin-bottom: 25px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: right;
|
||||
margin-top: 30px;
|
||||
color: #909090;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #CB2915;
|
||||
}
|
||||
|
||||
|
||||
.alert {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.feedback-aligner .alert {
|
||||
background-position: 1.27273em center;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 2px;
|
||||
border-width: 1px;
|
||||
color: #4D5258;
|
||||
display: inline-block;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.4em;
|
||||
margin: 0;
|
||||
padding: 0.909091em 3.63636em;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
}
|
||||
.alert.alert-success {
|
||||
background-color: #E4F1E1;
|
||||
border-color: #4B9E39;
|
||||
}
|
||||
.alert.alert-error {
|
||||
background-color: #F8E7E7;
|
||||
border-color: #B91415;
|
||||
}
|
||||
.alert.alert-warning {
|
||||
background-color: #FEF1E9;
|
||||
border-color: #F17528;
|
||||
}
|
||||
.alert.alert-info {
|
||||
background-color: #E4F3FA;
|
||||
border-color: #5994B2;
|
||||
}
|
||||
|
||||
.form-horizontal {
|
||||
border-top: 1px solid #E9E8E8;
|
||||
padding-top: 23px;
|
||||
}
|
||||
|
||||
.form-horizontal .control-label {
|
||||
color: #909090;
|
||||
line-height: 1.4em;
|
||||
padding-top: 5px;
|
||||
position: relative;
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.control-label + .required {
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#kc-form-buttons {
|
||||
text-align: right;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#kc-form-buttons .btn-primary {
|
||||
float: right;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Authenticator page */
|
||||
|
||||
ol {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
ol li {
|
||||
font-size: 13px;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ol li img {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
hr + .form-horizontal {
|
||||
border: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.kc-dropdown{
|
||||
position: relative;
|
||||
}
|
||||
.kc-dropdown > a{
|
||||
display:block;
|
||||
padding: 11px 10px 12px;
|
||||
line-height: 12px;
|
||||
font-size: 12px;
|
||||
color: #fff !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
.kc-dropdown > a::after{
|
||||
content: "\2c5";
|
||||
margin-left: 4px;
|
||||
}
|
||||
.kc-dropdown:hover > a{
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
}
|
||||
.kc-dropdown ul li a{
|
||||
padding: 1px 11px;
|
||||
font-size: 12px;
|
||||
color: #000 !important;
|
||||
border: 1px solid #fff;
|
||||
text-decoration: none;
|
||||
display:block;
|
||||
line-height: 20px;
|
||||
}
|
||||
.kc-dropdown ul li a:hover{
|
||||
color: #4d5258;
|
||||
background-color: #d4edfa;
|
||||
border-color: #b3d3e7;
|
||||
}
|
||||
.kc-dropdown ul{
|
||||
position: absolute;
|
||||
z-index: 2000;
|
||||
list-style:none;
|
||||
display:none;
|
||||
padding: 5px 0px;
|
||||
margin: 0px;
|
||||
background-color: #fff !important;
|
||||
border: 1px solid #b6b6b6;
|
||||
border-radius: 1px;
|
||||
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
background-clip: padding-box;
|
||||
min-width: 100px;
|
||||
}
|
||||
.kc-dropdown:hover ul{
|
||||
display:block;
|
||||
}
|
||||
|
||||
|
||||
#kc-totp-secret-key {
|
||||
border: 1px solid #eee;
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
margin: 50px 0;
|
||||
}
|
BIN
account/resources/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 202 B |
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 4.1 KiB |
15
account/resources/index.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
<script type="module" crossorigin src="/assets/index-1537da3e.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-9c371cad.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
1
account/resources/logo.svg
Normal file
After Width: | Height: | Size: 6.5 KiB |
25
account/resources/manifest.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"index.css": {
|
||||
"file": "assets/index-9c371cad.css",
|
||||
"src": "index.css"
|
||||
},
|
||||
"index.html": {
|
||||
"css": [
|
||||
"assets/index-9c371cad.css"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"src/views/Home.tsx"
|
||||
],
|
||||
"file": "assets/index-1537da3e.js",
|
||||
"isEntry": true,
|
||||
"src": "index.html"
|
||||
},
|
||||
"src/views/Home.tsx": {
|
||||
"file": "assets/Home-7c18ddb3.js",
|
||||
"imports": [
|
||||
"index.html"
|
||||
],
|
||||
"isDynamicEntry": true,
|
||||
"src": "src/views/Home.tsx"
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='sessions' bodyClass='sessions'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("sessionsHtmlTitle")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="ps-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>${msg("ip")}</td>
|
||||
<td>${msg("started")}</td>
|
||||
<td>${msg("lastAccess")}</td>
|
||||
<td>${msg("expires")}</td>
|
||||
<td>${msg("clients")}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<#list sessions.sessions as session>
|
||||
<tr>
|
||||
<td>${session.ipAddress}</td>
|
||||
<td>${session.started?datetime}</td>
|
||||
<td>${session.lastAccess?datetime}</td>
|
||||
<td>${session.expires?datetime}</td>
|
||||
<td>
|
||||
<#list session.clients as client>
|
||||
${client}<br/>
|
||||
</#list>
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<form action="${url.sessionsUrl}" method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<button
|
||||
id="logout-all-sessions"
|
||||
class="ps-button"
|
||||
>${msg("doLogOutAllSessions")}</button>
|
||||
</form>
|
||||
|
||||
</@layout.mainLayout>
|
|
@ -1,151 +0,0 @@
|
|||
<#macro mainLayout active bodyClass>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<title>${msg("accountManagementTitle")}</title>
|
||||
|
||||
<link rel="icon" href="${url.resourcesPath}/img/pub.solar.svg" />
|
||||
|
||||
<link href="${url.resourcesPath}/css/index.css?v4" rel="stylesheet" />
|
||||
|
||||
<script defer src="${url.resourcesPath}/js/background.js" type="text/javascript"></script>
|
||||
<script defer src="${url.resourcesPath}/js/i18n.js" type="text/javascript"></script>
|
||||
|
||||
<#if properties.stylesCommon?has_content>
|
||||
<#list properties.stylesCommon?split(' ') as style>
|
||||
<link href="${url.resourcesCommonPath}/${style}" rel="stylesheet" />
|
||||
</#list>
|
||||
</#if>
|
||||
<#if properties.styles?has_content>
|
||||
<#list properties.styles?split(' ') as style>
|
||||
<link href="${url.resourcesPath}/${style}" rel="stylesheet" />
|
||||
</#list>
|
||||
</#if>
|
||||
<#if properties.scripts?has_content>
|
||||
<#list properties.scripts?split(' ') as script>
|
||||
<script defer src="${url.resourcesPath}/${script}" type="text/javascript"></script>
|
||||
</#list>
|
||||
</#if>
|
||||
<#if properties.scriptsCommon?has_content>
|
||||
<#list properties.scriptsCommon?split(' ') as script>
|
||||
<script defer src="${url.resourcesCommonPath}/${script}" type="text/javascript"></script>
|
||||
</#list>
|
||||
</#if>
|
||||
</head>
|
||||
<body class="ps-main ps-main_full">
|
||||
<div
|
||||
id="background"
|
||||
class="ps-background ps-main--background"
|
||||
>
|
||||
<div
|
||||
id="x1312"
|
||||
class="ps-background--1312"
|
||||
>0x1312</div>
|
||||
<div
|
||||
id="logo"
|
||||
class="ps-background--logo ps-logo"
|
||||
>
|
||||
<img
|
||||
class="ps-logo--base"
|
||||
src="${url.resourcesPath}/img/pub.solar.svg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<main class="ps-main--page ps-page">
|
||||
<header class="ps-page--header ps-header">
|
||||
<a href="https://pub.solar/" class="ps-homelink">pub.solar/</a>
|
||||
|
||||
<#if realm.internationalizationEnabled && locale.supported?size gt 1>
|
||||
<select
|
||||
class="ps-i18n-links ps-header--i18n"
|
||||
id="language-toggle"
|
||||
>
|
||||
<#list locale.supported as l>
|
||||
<option
|
||||
value="${l.url}"
|
||||
<#if locale.current == l.label>selected</#if>
|
||||
>${l.label}</option>
|
||||
</#list>
|
||||
</select>
|
||||
</#if>
|
||||
|
||||
<nav class="ps-header--nav" role="navigation">
|
||||
<ul class="ps-header--nav-list">
|
||||
<#if referrer?has_content && referrer.url?has_content>
|
||||
<li class="ps-header--nav-item">
|
||||
<a
|
||||
href="${referrer.url}"
|
||||
class="ps-link"
|
||||
id="referrer"
|
||||
>${msg("backTo",referrer.name)}</a>
|
||||
</li>
|
||||
</#if>
|
||||
<li class="ps-header--nav-item">
|
||||
<a
|
||||
href="${url.getLogoutUrl()}"
|
||||
class="ps-link"
|
||||
>${msg("doSignOut")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section class="ps-page--section ps-page--section_full">
|
||||
<nav class="ps-section-nav">
|
||||
<ul class="ps-section-nav--list">
|
||||
<li class="ps-section-nav--link <#if active=='account'>ps-section-nav--link_active</#if>">
|
||||
<a href="${url.accountUrl}">${msg("account")}</a>
|
||||
</li>
|
||||
<#if features.passwordUpdateSupported>
|
||||
<li class="ps-section-nav--link <#if active=='password'>ps-section-nav--link_active</#if>">
|
||||
<a href="${url.passwordUrl}">${msg("password")}</a>
|
||||
</li>
|
||||
</#if>
|
||||
<li class="ps-section-nav--link <#if active=='totp'>ps-section-nav--link_active</#if>">
|
||||
<a href="${url.totpUrl}">${msg("authenticator")}</a>
|
||||
</li>
|
||||
<#if features.identityFederation>
|
||||
<li class="ps-section-nav--link <#if active=='social'>ps-section-nav--link_active</#if>">
|
||||
<a href="${url.socialUrl}">${msg("federatedIdentity")}</a>
|
||||
</li>
|
||||
</#if>
|
||||
<li class="ps-section-nav--link <#if active=='sessions'>ps-section-nav--link_active</#if>">
|
||||
<a href="${url.sessionsUrl}">${msg("sessions")}</a>
|
||||
</li>
|
||||
<li class="ps-section-nav--link <#if active=='applications'>ps-section-nav--link_active</#if>">
|
||||
<a href="${url.applicationsUrl}">${msg("applications")}</a>
|
||||
</li>
|
||||
<#if features.log>
|
||||
<li class="ps-section-nav--link <#if active=='log'>ps-section-nav--link_active</#if>">
|
||||
<a href="${url.logUrl}">${msg("log")}</a>
|
||||
</li>
|
||||
</#if>
|
||||
<#if realm.userManagedAccessAllowed && features.authorization>
|
||||
<li class="ps-section-nav--link <#if active=='authorization'>ps-section-nav--link_active</#if>">
|
||||
<a href="${url.resourceUrl}">${msg("myResources")}</a>
|
||||
</li>
|
||||
</#if>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="ps-page--section-contents">
|
||||
<#if message?has_content>
|
||||
<div class="alert alert-${message.type}">
|
||||
<#if message.type=='success' ><span class="pficon pficon-ok"></span></#if>
|
||||
<#if message.type=='error' ><span class="pficon pficon-error-circle-o"></span></#if>
|
||||
<span class="kc-feedback-text">${kcSanitize(message.summary)?no_esc}</span>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<#nested "content">
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</#macro>
|
|
@ -2,11 +2,3 @@ parent=keycloak.v2
|
|||
import=common/pub.solar
|
||||
|
||||
styles=
|
||||
|
||||
kcButtonClass=ps-button
|
||||
kcButtonPrimaryClass=ps-button_primary
|
||||
kcButtonLargeClass=ps-button_large
|
||||
|
||||
kcFormGroupClass=ps-form-group
|
||||
kcLabelClass=ps-form-group--label
|
||||
kcInputClass=ps-input
|
||||
|
|
131
account/totp.ftl
|
@ -1,131 +0,0 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='totp' bodyClass='totp'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("authenticatorTitle")}</h2>
|
||||
</div>
|
||||
<#if totp.otpCredentials?size == 0>
|
||||
<div class="col-md-2 subtitle">
|
||||
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
|
||||
</div>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<#if totp.enabled>
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<#if totp.otpCredentials?size gt 1>
|
||||
<tr>
|
||||
<th colspan="4">${msg("configureAuthenticators")}</th>
|
||||
</tr>
|
||||
<#else>
|
||||
<tr>
|
||||
<th colspan="3">${msg("configureAuthenticators")}</th>
|
||||
</tr>
|
||||
</#if>
|
||||
</thead>
|
||||
<tbody>
|
||||
<#list totp.otpCredentials as credential>
|
||||
<tr>
|
||||
<td class="provider">${msg("mobile")}</td>
|
||||
<#if totp.otpCredentials?size gt 1>
|
||||
<td class="provider">${credential.id}</td>
|
||||
</#if>
|
||||
<td class="provider">${credential.userLabel!}</td>
|
||||
<td class="action">
|
||||
<form action="${url.totpUrl}" method="post" class="form-inline">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<input type="hidden" id="submitAction" name="submitAction" value="Delete">
|
||||
<input type="hidden" id="credentialId" name="credentialId" value="${credential.id}">
|
||||
<button id="remove-mobile" class="ps-button ps-button_small">
|
||||
${msg("doRemove")}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
<#else>
|
||||
|
||||
<hr/>
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
<p>${msg("totpStep1")}</p>
|
||||
|
||||
<ul>
|
||||
<#list totp.supportedApplications as app>
|
||||
<li>${msg(app)}</li>
|
||||
</#list>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<#if mode?? && mode = "manual">
|
||||
<li>
|
||||
<p>${msg("totpManualStep2")}</p>
|
||||
<pre class="ps-mono"><code>${totp.totpSecretEncoded}</code></pre>
|
||||
<p><a href="${totp.qrUrl}" id="mode-barcode">${msg("totpScanBarcode")}</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>${msg("totpManualStep3")}</p>
|
||||
<ul>
|
||||
<li id="kc-totp-type">${msg("totpType")}: ${msg("totp." + totp.policy.type)}</li>
|
||||
<li id="kc-totp-algorithm">${msg("totpAlgorithm")}: ${totp.policy.getAlgorithmKey()}</li>
|
||||
<li id="kc-totp-digits">${msg("totpDigits")}: ${totp.policy.digits}</li>
|
||||
<#if totp.policy.type = "totp">
|
||||
<li id="kc-totp-period">${msg("totpInterval")}: ${totp.policy.period}</li>
|
||||
<#elseif totp.policy.type = "hotp">
|
||||
<li id="kc-totp-counter">${msg("totpCounter")}: ${totp.policy.initialCounter}</li>
|
||||
</#if>
|
||||
</ul>
|
||||
</li>
|
||||
<#else>
|
||||
<li>
|
||||
<p>${msg("totpStep2")}</p>
|
||||
<p><img src="data:image/png;base64, ${totp.totpSecretQrCode}" alt="Figure: Barcode"></p>
|
||||
<p><a href="${totp.manualUrl}" id="mode-manual">${msg("totpUnableToScan")}</a></p>
|
||||
</li>
|
||||
</#if>
|
||||
<li>
|
||||
<p>${msg("totpStep3")}</p>
|
||||
<p>${msg("totpStep3DeviceName")}</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<hr/>
|
||||
|
||||
<form action="${url.totpUrl}" class="ps-container" method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
<div class="ps-form-group">
|
||||
<label for="totp" class="ps-form-group--label">${msg("authenticatorCode")} <span class="required">*</span></label>
|
||||
|
||||
<input type="text" class="ps-input" id="totp" name="totp" autocomplete="off" autofocus>
|
||||
<input type="hidden" id="totpSecret" name="totpSecret" value="${totp.totpSecret}"/>
|
||||
</div>
|
||||
|
||||
<div class="ps-form-group" ${messagesPerField.printIfExists('userLabel',properties.kcFormGroupErrorClass!)}">
|
||||
<label for="userLabel" class="ps-form-group--label">${msg("totpDeviceName")} <#if totp.otpCredentials?size gte 1><span class="required">*</span></#if></label>
|
||||
|
||||
<input type="text" class="ps-input" id="userLabel" name="userLabel" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="ps-form-group">
|
||||
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
|
||||
<div class="">
|
||||
<button type="submit"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}"
|
||||
id="saveTOTPBtn" name="submitAction" value="Save">${msg("doSave")}
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}"
|
||||
id="cancelTOTPBtn" name="submitAction" value="Cancel">${msg("doCancel")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</#if>
|
||||
|
||||
</@layout.mainLayout>
|
|
@ -56,6 +56,7 @@
|
|||
google-font-downloader
|
||||
compile-sass
|
||||
watch-sass
|
||||
nodejs
|
||||
];
|
||||
};
|
||||
};
|
||||
|
|