Initial commit
This commit is contained in:
commit
9b9ec3472c
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
tags
|
||||
.dev-import
|
27
README.md
Normal file
27
README.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
# pub.solar Keycloak theme
|
||||
|
||||
|
||||
## Development setup
|
||||
|
||||
To start a Dev Keycloak instance that can show the pub.solar theme, you need to do the following:
|
||||
|
||||
1. Go into Keycloak's [Administration Console](https://auth.pub.solar/admin), and export the `pub.solar` realm config. In Keycloak, open the pub.solar realm, click on the menu item "Realm settings", open the dropdown "Action", and click "Partial export". Move the generated JSON file into `./.dev-imports` in this repo.
|
||||
|
||||
2. Run the following command:
|
||||
```
|
||||
docker run \
|
||||
--name keycloak-theme-dev \
|
||||
-p 8080:8080 \
|
||||
-e KEYCLOAK_ADMIN=admin \
|
||||
-e KEYCLOAK_ADMIN_PASSWORD=admin \
|
||||
-v $(pwd)/keycloak:/opt/keycloak/themes/pub.solar \
|
||||
-v $(pwd)/.dev-import:/opt/keycloak/data/import \
|
||||
quay.io/keycloak/keycloak:23.0.1 \
|
||||
start-dev --import-realm --features="declarative-user-profile"
|
||||
```
|
||||
|
||||
3. After this, you can start and stop the container using `docker start keycloak-theme-dev` and `docker-stop keycloak-theme-dev`.
|
||||
|
||||
4. Connect to the local Keycloak instance at http://localhost:8080 and open the Administration Console. Login with username: `admin`, password: `admin`.
|
||||
|
||||
5. To view the theme in action, open http://localhost:8080/realms/pub.solar/account
|
98
flake.lock
Normal file
98
flake.lock
Normal file
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
"nodes": {
|
||||
"devshell": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1688380630,
|
||||
"narHash": "sha256-8ilApWVb1mAi4439zS3iFeIT0ODlbrifm/fegWwgHjA=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "f9238ec3d75cefbb2b42a44948c4e8fb1ae9a205",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1689068808,
|
||||
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1689282004,
|
||||
"narHash": "sha256-VNhuyb10c9SV+3hZOlxwJwzEGytZ31gN9w4nPCnNvdI=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e74e68449c385db82de3170288a28cd0f608544f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devshell": "devshell",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
51
flake.nix
Normal file
51
flake.nix
Normal file
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
description = "Keycloak pub.solar theme";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
devshell.url = "github:numtide/devshell";
|
||||
devshell.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs = { self, flake-utils, devshell, nixpkgs }:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
overlays = [
|
||||
devshell.overlay
|
||||
(import ./overlay.nix)
|
||||
];
|
||||
};
|
||||
in
|
||||
flake-utils.lib.simpleFlake {
|
||||
inherit self nixpkgs;
|
||||
name = "keycloak-theme-pub-solar";
|
||||
overlay = ./overlay.nix;
|
||||
preOverlays = [ devshell.overlays.default ];
|
||||
shell = { pkgs }:
|
||||
let
|
||||
google-font-downloader = with pkgs; writeShellScriptBin "google-font-downloader" ''
|
||||
echo "Attempting to fetch $1"
|
||||
${nodejs}/bin/npx google-font-downloader -- $1
|
||||
rm ./common/resources/scss/typography.scss
|
||||
echo "/* To regenerate this file, run $ google-font-downloader '$1' */" >> ./common/resources/scss/typography.scss
|
||||
echo "" >> ./common/resources/scss/typography.scss
|
||||
cat ./google-fonts-*.css >> ./common/resources/scss/typography.scss
|
||||
rm ./google-fonts-*.css
|
||||
mv fonts ./common/resources/fonts
|
||||
'';
|
||||
in
|
||||
pkgs.devshell.mkShell {
|
||||
imports = [ ];
|
||||
|
||||
# Add additional packages you'd like to be available in your devshell
|
||||
# PATH here
|
||||
devshell.packages = with pkgs; [
|
||||
google-font-downloader
|
||||
nodejs
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
25
frontend/.eslintrc.cjs
Normal file
25
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
frontend/.gitignore
vendored
Normal file
28
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
frontend/.prettierrc.json
Normal file
8
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
frontend/README.md
Normal file
68
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
frontend/cypress.config.ts
Normal file
8
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
frontend/cypress/e2e/example.cy.ts
Normal file
8
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
frontend/cypress/e2e/tsconfig.json
Normal file
10
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
frontend/cypress/fixtures/example.json
Normal file
5
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
frontend/cypress/support/commands.ts
Normal file
39
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
frontend/cypress/support/e2e.ts
Normal file
20
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')
|
17
frontend/env.d.ts
vendored
Normal file
17
frontend/env.d.ts
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
interface GlobalKeycloakData {
|
||||
developmentMode: boolean;
|
||||
authUrl: string;
|
||||
baseUrl: string;
|
||||
realm: string;
|
||||
resourceUrl: string;
|
||||
isReactLoading: boolean;
|
||||
|
||||
brandImg: string;
|
||||
brandUrl: string;
|
||||
};
|
||||
|
||||
interface Window {
|
||||
globals: InitialData;
|
||||
}
|
13
frontend/index.html
Normal file
13
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>
|
7883
frontend/package-lock.json
generated
Normal file
7883
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
48
frontend/package.json
Normal file
48
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
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
1
frontend/public/logo.svg
Normal file
1
frontend/public/logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.5 KiB |
12
frontend/src/App.tsx
Normal file
12
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
frontend/src/assets/base.css
Normal file
86
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
frontend/src/assets/main.css
Normal file
35
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
frontend/src/components/Background.scss
Normal file
35
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
frontend/src/components/Background.tsx
Normal file
45
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="/public/logo.svg"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>;
|
||||
},
|
||||
};
|
45
frontend/src/components/Logo.scss
Normal file
45
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
frontend/src/components/__tests__/HelloWorld.spec.ts
Normal file
11
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')
|
||||
})
|
||||
})
|
17
frontend/src/keycloak.ts
Normal file
17
frontend/src/keycloak.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import Keycloak from 'keycloak-js';
|
||||
|
||||
const keycloak = new Keycloak({
|
||||
url: `http://keycloak-server${kc_base_path}`,
|
||||
realm,
|
||||
clientId,
|
||||
});
|
||||
|
||||
try {
|
||||
const authenticated = await keycloak.init({
|
||||
onLoad: 'check-sso',
|
||||
silentCheckSsoRedirectUri: `${location.origin}/silent-check-sso.html`
|
||||
});
|
||||
console.log(`User is ${authenticated ? 'authenticated' : 'not authenticated'}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize adapter:', error);
|
||||
}
|
14
frontend/src/main.ts
Normal file
14
frontend/src/main.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
import App from './App';
|
||||
import router from './router';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
|
||||
app.mount('#app');
|
22
frontend/src/router/index.ts
Normal file
22
frontend/src/router/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
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'),
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/AboutView.vue')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
12
frontend/src/stores/counter.ts
Normal file
12
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 }
|
||||
})
|
11
frontend/src/views/Home.tsx
Normal file
11
frontend/src/views/Home.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import TheWelcome from '../components/TheWelcome.vue'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
return () => (
|
||||
<main>
|
||||
<TheWelcome />
|
||||
</main>
|
||||
);
|
||||
},
|
||||
};
|
9
frontend/src/views/HomeView.vue
Normal file
9
frontend/src/views/HomeView.vue
Normal file
|
@ -0,0 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import TheWelcome from '../components/TheWelcome.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<TheWelcome />
|
||||
</main>
|
||||
</template>
|
12
frontend/tsconfig.app.json
Normal file
12
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
frontend/tsconfig.json
Normal file
14
frontend/tsconfig.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.vitest.json"
|
||||
}
|
||||
]
|
||||
}
|
16
frontend/tsconfig.node.json
Normal file
16
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
frontend/tsconfig.vitest.json
Normal file
9
frontend/tsconfig.vitest.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"lib": [],
|
||||
"types": ["node", "jsdom"]
|
||||
}
|
||||
}
|
25
frontend/vite.config.ts
Normal file
25
frontend/vite.config.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
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)),
|
||||
},
|
||||
},
|
||||
|
||||
server: {
|
||||
proxy: {
|
||||
'/realms': 'http://localhost:8080',
|
||||
'/silent-check-sso.html': 'http://localhost:8080',
|
||||
},
|
||||
},
|
||||
});
|
14
frontend/vitest.config.ts
Normal file
14
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))
|
||||
}
|
||||
})
|
||||
)
|
76
keycloak/account/index.ftl
Normal file
76
keycloak/account/index.ftl
Normal file
|
@ -0,0 +1,76 @@
|
|||
<!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>
|
||||
const globals = {
|
||||
<#if properties.developmentMode?has_content && properties.developmentMode == "true">
|
||||
developmentMode: true,
|
||||
<#else>
|
||||
developmentMode: false,
|
||||
</#if>
|
||||
authUrl: '${authUrl}',
|
||||
baseUrl: '${baseUrl}',
|
||||
realm: '${realm.name}',
|
||||
resourceUrl: '${resourceUrl}',
|
||||
isReactLoading: false,
|
||||
|
||||
<#if properties.logo?has_content>
|
||||
brandImg: resourceUrl + '${properties.logo}',
|
||||
<#else>
|
||||
brandImg: resourceUrl + '/public/logo.svg',
|
||||
</#if>
|
||||
|
||||
<#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>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
19
keycloak/account/theme.properties
Normal file
19
keycloak/account/theme.properties
Normal file
|
@ -0,0 +1,19 @@
|
|||
parent=base
|
||||
deprecatedMode=false
|
||||
|
||||
scripts=
|
||||
|
||||
developmentMode=true
|
||||
|
||||
# This is the logo in upper lefthand corner.
|
||||
# It must be a relative path.
|
||||
logo=/public/logo.svg
|
||||
|
||||
# This is the link followed when clicking on the logo.
|
||||
# It can be any valid URL, including an external site.
|
||||
logoUrl=./
|
||||
|
||||
# This is the icon for the account console.
|
||||
# It must be a relative path.
|
||||
favIcon=/public/favicon.ico
|
||||
|
19
keycloak/login/theme.properties
Normal file
19
keycloak/login/theme.properties
Normal file
|
@ -0,0 +1,19 @@
|
|||
parent=base
|
||||
deprecatedMode=false
|
||||
|
||||
scripts=
|
||||
|
||||
developmentMode=true
|
||||
|
||||
# This is the logo in upper lefthand corner.
|
||||
# It must be a relative path.
|
||||
logo=/public/logo.svg
|
||||
|
||||
# This is the link followed when clicking on the logo.
|
||||
# It can be any valid URL, including an external site.
|
||||
logoUrl=./
|
||||
|
||||
# This is the icon for the account console.
|
||||
# It must be a relative path.
|
||||
favIcon=/public/favicon.ico
|
||||
|
10
overlay.nix
Normal file
10
overlay.nix
Normal file
|
@ -0,0 +1,10 @@
|
|||
final: prev:
|
||||
let
|
||||
pkgs = final;
|
||||
in
|
||||
{
|
||||
# this key should be the same as the simpleFlake name attribute.
|
||||
keycloak-theme-pub-solar = {
|
||||
keycloak-theme-pub-solar = import ./pkgs/keycloak-theme-pub-solar.nix { inherit pkgs; };
|
||||
};
|
||||
}
|
18
pkgs/keycloak-theme-pub-solar.nix
Normal file
18
pkgs/keycloak-theme-pub-solar.nix
Normal file
|
@ -0,0 +1,18 @@
|
|||
{ pkgs, ... }:
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "keycloak-theme-pub-solar";
|
||||
version = "0.0.0";
|
||||
src = pkgs.lib.cleanSource ../src;
|
||||
|
||||
installPhase = ''
|
||||
${pkgs.sassc}/bin/sassc "./common/resources/scss/index.scss" > "./common/resources/css/index.css"
|
||||
|
||||
mkdir $out
|
||||
cp -r account $out/
|
||||
cp -r login $out/
|
||||
cp -r admin $out/
|
||||
cp -r common $out/
|
||||
cp -r welcome $out/
|
||||
cp -r email $out/
|
||||
'';
|
||||
}
|
Loading…
Reference in a new issue