diff --git a/Dockerfile b/Dockerfile index 447a431be0..fab73c3fb4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -84,6 +84,12 @@ COPY --from=builder /usr/local/src/dex/api/v2/go.mod /usr/local/src/dex/api/v2/g COPY --from=builder /go/bin/dex /usr/local/bin/dex COPY --from=builder /go/bin/docker-entrypoint /usr/local/bin/docker-entrypoint COPY --from=builder /usr/local/src/dex/web /srv/dex/web +# Testkube sign-in pages (see testkube/README.md). The templates replace the upstream ones; +# the static assets live under static/testkube/. DEX_FRONTEND_DIR makes Dex read this +# directory without any frontend.dir configuration. +COPY --from=builder /usr/local/src/dex/testkube/web/templates /srv/dex/web/templates +COPY --from=builder /usr/local/src/dex/testkube/web/static/testkube /srv/dex/web/static/testkube +ENV DEX_FRONTEND_DIR=/srv/dex/web COPY --from=gomplate /usr/local/bin/gomplate /usr/local/bin/gomplate diff --git a/testkube/README.md b/testkube/README.md new file mode 100644 index 0000000000..a8bdc1905f --- /dev/null +++ b/testkube/README.md @@ -0,0 +1,14 @@ +# Testkube sign-in pages + +`web/` holds the Testkube look for the Dex pages: nine templates that replace the upstream +templates, and the static assets (CSS, wordmark, favicon, Roboto fonts, background images) +served at `/static/testkube/`. The Dockerfile copies them into `/srv/dex/web` and sets +`DEX_FRONTEND_DIR`, so the image shows these pages with no `frontend` configuration. + +The values in `web/static/testkube/styles.css` copy the Testkube dashboard build 1:1 +(`src/styles/Colors.ts`, the `AuthPage*`, `Banner`, and `AuthForm*` styled-components, the +antd theme tokens, `src/styles/MediaQueries.ts` in `testkube-cloud-api/js/packages/web`). +The header comment in `styles.css` lists the sources. + +The only per-environment value is `frontend.extra.dashboardURL` in the Dex config. The error +page uses it for the "Back to Testkube" button. Without it the button returns to the previous page. diff --git a/testkube/web/static/testkube/favicon.png b/testkube/web/static/testkube/favicon.png new file mode 100644 index 0000000000..600fb8dc01 Binary files /dev/null and b/testkube/web/static/testkube/favicon.png differ diff --git a/testkube/web/static/testkube/home-left.png b/testkube/web/static/testkube/home-left.png new file mode 100644 index 0000000000..fbf9955338 Binary files /dev/null and b/testkube/web/static/testkube/home-left.png differ diff --git a/testkube/web/static/testkube/home-right.png b/testkube/web/static/testkube/home-right.png new file mode 100644 index 0000000000..dc44b53dee Binary files /dev/null and b/testkube/web/static/testkube/home-right.png differ diff --git a/testkube/web/static/testkube/roboto-v30-latin-400.woff2 b/testkube/web/static/testkube/roboto-v30-latin-400.woff2 new file mode 100644 index 0000000000..020729ef8d Binary files /dev/null and b/testkube/web/static/testkube/roboto-v30-latin-400.woff2 differ diff --git a/testkube/web/static/testkube/roboto-v30-latin-500.woff2 b/testkube/web/static/testkube/roboto-v30-latin-500.woff2 new file mode 100644 index 0000000000..29342a8de2 Binary files /dev/null and b/testkube/web/static/testkube/roboto-v30-latin-500.woff2 differ diff --git a/testkube/web/static/testkube/roboto-v30-latin-700.woff2 b/testkube/web/static/testkube/roboto-v30-latin-700.woff2 new file mode 100644 index 0000000000..771fbeccc6 Binary files /dev/null and b/testkube/web/static/testkube/roboto-v30-latin-700.woff2 differ diff --git a/testkube/web/static/testkube/styles.css b/testkube/web/static/testkube/styles.css new file mode 100644 index 0000000000..f31b1747ca --- /dev/null +++ b/testkube/web/static/testkube/styles.css @@ -0,0 +1,332 @@ +/* + * Testkube theme for Dex. + * + * Every value in this file comes from the enterprise dashboard build: + * - colors: assets/Colors-*.js (K.mainBackground, K.indigo400, K.slate*, K.rose*, ...) + * - layout: styled-components AuthPageWrapper, AuthPageContentWrapper, AuthPageBGWrapper, + * AuthPageFormWrapper, StandaloneFormWrapper, AuthPageButtonsWrapper, + * BrandWordmark, Banner, AuthFormTitle, AuthFormSubtitle (assets/molecules-*.js) + * - controls: the antd theme (token.colorPrimary = K.purple, borderRadius 4, + * components.Input controlHeight 44, components.Button controlHeight 40) + * - breakpoints: assets/MediaQueries-*.js (tablet = max-width 768px, laptop = max-width 1024px) + * Dex serves this directory at /static/testkube/. Source: kubeshop/dex, directory testkube/web/, so all asset URLs are relative to this file. + */ + +@font-face { font-display: swap; font-family: 'Roboto'; font-style: normal; font-weight: 400; src: url('roboto-v30-latin-400.woff2') format('woff2'); } +@font-face { font-display: swap; font-family: 'Roboto'; font-style: normal; font-weight: 500; src: url('roboto-v30-latin-500.woff2') format('woff2'); } +@font-face { font-display: swap; font-family: 'Roboto'; font-style: normal; font-weight: 700; src: url('roboto-v30-latin-700.woff2') format('woff2'); } + +:root { + --tk-main-background: #111827; /* K.mainBackground */ + --tk-white-pure: #FFFFFF; /* K.whitePure */ + --tk-purple: #7984F4; /* antd colorPrimary, colorLink */ + --tk-link-hover: #a8b2ff; /* antd colorLinkHover */ + --tk-link-active: #5d63cf; /* antd colorLinkActive */ + --tk-indigo-300: #A5B4FC; /* K.indigo300: primary button hover text */ + --tk-indigo-400: #818CF8; /* K.indigo400: card border, primary button */ + --tk-slate-200: #E2E8F0; + --tk-slate-300: #CBD5E1; + --tk-slate-400: #94A3B8; + --tk-slate-500: #64748B; + --tk-slate-600: #475569; + --tk-slate-700: #334155; + --tk-slate-800: #1E293B; + --tk-pink-500: #EC4899; /* antd colorError */ + --tk-violet-800: #5B21B6; + --tk-rose-200: #FECDD3; + --tk-rose-500: #F43F5E; + --tk-font: 'Roboto', sans-serif; + --tk-ease: cubic-bezier(0.645, 0.045, 0.355, 1); /* antd motionEaseInOut */ +} + +*, *::before, *::after { box-sizing: border-box; } +html, body { width: 100%; height: 100%; margin: 0; } +.theme-body { + background-color: var(--tk-main-background); + color: var(--tk-white-pure); + font-family: var(--tk-font); + font-size: 14px; + line-height: 22px; + -webkit-font-smoothing: antialiased; +} + +/* AuthPageBGWrapper + AuthPageBGPartLeft/Right */ +.tk-bg { + position: fixed; + inset: 0; + z-index: 1; + display: flex; + justify-content: space-between; + height: 100%; + width: 100%; + background-color: #111827; +} +.tk-bg__part { + background-repeat: no-repeat; + background-size: cover; + background-position: 50% 50%; + mix-blend-mode: luminosity; + opacity: 0.5; +} +.tk-bg__part--left { width: 525px; background-image: url('home-left.png'); } +.tk-bg__part--right { width: 475px; background-image: url('home-right.png'); } +@media (max-width: 1024px) { + .tk-bg__part { width: 100%; background-size: cover; } + .tk-bg__part:last-child { display: none; } +} + +/* Page root (display:flex; min-height 100vh) + AuthPageWrapper */ +.tk-root { display: flex; min-height: 100%; } +.tk-page { + position: relative; + z-index: 2; + display: flex; + width: 100%; + margin: auto; + align-items: center; +} + +/* AuthPageContentWrapper */ +.tk-content { + z-index: 2; + display: flex; + flex-direction: column; + gap: 90px; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + padding-top: 35px; +} +@media (max-width: 768px) { + .tk-content { gap: 40px; padding-top: 0; } +} + +/* BrandWordmark */ +.tk-wordmark { display: flex; justify-content: center; } +.tk-wordmark img { width: 205px; height: auto; display: block; } + +/* AuthPageFormWrapper + StandaloneFormWrapper */ +.tk-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + gap: 60px; + border: 1px solid var(--tk-indigo-400); + border-radius: 4px; + padding: 45px clamp(20px, 6vw, 55px) 94px; + max-width: 100%; +} +.tk-card--alt-link { padding-bottom: 48px; } +@media (max-width: 768px) { + .tk-card { gap: 40px; padding-top: 32px; padding-bottom: 48px; } + .tk-card--alt-link { padding-bottom: 32px; } +} + +/* AuthFormTitle, AuthFormSubtitle */ +.tk-card__title { + margin: 0; + font-size: 34px; + font-weight: 700; + line-height: 40px; + text-align: center; + color: var(--tk-white-pure); +} +.tk-card__subtitle { + margin: 24px 0 0; + text-align: center; + color: var(--tk-slate-400); + font-size: 18px; + font-weight: 400; + line-height: 24px; +} + +/* Banner (+ BannerTitle, BannerText) */ +.tk-banner { + width: 650px; + max-width: 100%; + min-height: 80px; + padding: 20px; + margin-bottom: 24px; + border: 1px solid; + border-radius: 8px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + text-align: center; + font-size: 14px; + line-height: 20px; +} +.tk-banner--error { background: #88133799; border-color: var(--tk-rose-500); color: var(--tk-rose-200); } +.tk-banner__title { font-weight: 500; } +.tk-banner__text { font-weight: 400; overflow-wrap: anywhere; } + +/* Card plus its error banner. The banner sits under the card, at the card's width, outside the + layout flow: the wordmark and the card keep their position when an error appears. On short + viewports the banner returns to the flow so it stays visible. */ +.tk-stack { position: relative; display: flex; flex-direction: column; align-items: stretch; } +.tk-stack .tk-banner { + position: absolute; + top: 100%; + left: 0; + right: 0; + width: auto; + margin: 24px 0 0; +} +@media (max-height: 720px) { + .tk-stack .tk-banner { position: static; width: 100%; } +} + +/* AuthPageButtonsWrapper. The dashboard's Login card renders its button 220px wide. */ +.tk-actions { + display: flex; + flex-direction: column; + gap: 16px; + width: 220px; + max-width: 100%; +} + +/* AltLinkText under the actions (StandaloneFormWrapper: margin-top -30px): white text, purple link */ +.tk-alt { + margin-top: -30px; + text-align: center; + color: var(--tk-white-pure); + font-size: 14px; + line-height: 22px; +} +.tk-alt a { color: var(--tk-purple); text-decoration: none; transition: color 0.2s var(--tk-ease); } +.tk-alt a:hover { color: var(--tk-link-hover); } +.tk-alt a:active { color: var(--tk-link-active); } + +/* antd Button (components.Button: controlHeight 40, no shadow, defaultBg rgba(255,255,255,0.05)) */ +.tk-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + height: 40px; + padding: 0 15px; + border: 1px solid transparent; + border-radius: 4px; + font-family: var(--tk-font); + font-size: 14px; + font-weight: 400; + line-height: 22px; + white-space: nowrap; + cursor: pointer; + text-decoration: none; + box-shadow: none; + transition: all 0.2s var(--tk-ease); +} +.tk-btn:focus-visible { outline: 4px solid rgba(121, 132, 244, 0.3); outline-offset: 1px; } +/* AntdCustomStyledButton ($customType primary): on hover, focus and active the fill drops + to transparent and the text turns indigo300 with an indigo400 border. */ +.tk-btn--primary { background-color: var(--tk-indigo-400); color: var(--tk-white-pure); } +.tk-btn--primary:hover, +.tk-btn--primary:focus, +.tk-btn--primary:active { background-color: transparent; color: var(--tk-indigo-300); border-color: var(--tk-indigo-400); } +/* Provider buttons, colors as on app.testkube.io: GitHub slate700, GitLab violet800, Google #4285F4, + every other connector (SSO, OIDC, SAML, LDAP, local) indigo400 like "Login via SSO". + --tk-btn-color holds the provider color: it fills the button, and it draws the border on + hover, focus and active, where the fill turns transparent and the text stays white. */ +.tk-btn--provider { --tk-btn-color: var(--tk-indigo-400); background-color: var(--tk-btn-color); color: var(--tk-white-pure); } +.tk-btn--github { --tk-btn-color: var(--tk-slate-700); } +.tk-btn--gitlab { --tk-btn-color: var(--tk-violet-800); } +.tk-btn--google { --tk-btn-color: #4285F4; } +.tk-btn--provider:hover, +.tk-btn--provider:focus, +.tk-btn--provider:active { background-color: transparent; border-color: var(--tk-btn-color); } + +.tk-btn--default { background-color: rgba(255, 255, 255, 0.05); color: var(--tk-slate-300); } +.tk-btn--default:hover { color: var(--tk-link-hover); border-color: var(--tk-link-hover); } +.tk-btn--default:active { color: var(--tk-link-active); border-color: var(--tk-link-active); } +.tk-btn:disabled { + cursor: not-allowed; + color: var(--tk-slate-500); + background-color: #1e293b80; + border-color: var(--tk-slate-800); +} +.tk-btn__icon { display: inline-flex; width: 14px; height: 14px; } +.tk-btn__icon svg { width: 1em; height: 1em; font-size: 14px; fill: currentColor; } + +/* antd Input (components.Input: controlHeight 44, slate800 background and border, + slate200 text, slate500 placeholder, slate600 hover border, colorPrimary focus border) */ +/* Form fields stack with the same 16px gap as AuthPageButtonsWrapper on app.testkube.io. */ +.tk-form { display: flex; flex-direction: column; gap: 16px; width: 340px; max-width: 100%; } +.tk-input { + width: 100%; + height: 44px; + padding: 4px 11px; + background-color: var(--tk-slate-800); + border: 1px solid var(--tk-slate-800); + border-radius: 4px; + color: var(--tk-slate-200); + font-family: var(--tk-font); + font-size: 14px; + line-height: 22px; + outline: none; + transition: all 0.2s; +} +.tk-input::placeholder { color: var(--tk-slate-500); } +.tk-input:hover { border-color: var(--tk-slate-600); } +.tk-input:focus { border-color: var(--tk-purple); box-shadow: 0 0 0 2px rgba(121, 132, 244, 0.2); } +/* antd status="error": border in colorError (K.pink500). */ +.tk-input--error, +.tk-input--error:hover { border-color: var(--tk-pink-500); } +.tk-input--error:focus { border-color: var(--tk-pink-500); box-shadow: 0 0 0 2px rgba(236, 72, 153, 0.2); } + +/* antd Input.Password: affix wrapper with the visibility toggle at the right (colorIcon slate500, hover slate400). */ +.tk-affix { position: relative; } +.tk-input--affix { padding-right: 36px; } +.tk-affix__toggle { + position: absolute; + top: 50%; + right: 11px; + transform: translateY(-50%); + display: flex; + align-items: center; + padding: 0; + border: 0; + background: none; + color: var(--tk-slate-500); + cursor: pointer; + transition: color 0.2s var(--tk-ease); +} +.tk-affix__toggle:hover { color: var(--tk-slate-400); } +.tk-affix__toggle:focus-visible { outline: 2px solid rgba(121, 132, 244, 0.5); outline-offset: 2px; border-radius: 2px; } +.tk-affix__icon { display: inline-flex; } +.tk-affix__icon[hidden] { display: none; } +.tk-affix__icon svg { width: 14px; height: 14px; fill: currentColor; } + +.tk-input:-webkit-autofill, +.tk-input:-webkit-autofill:focus { + -webkit-text-fill-color: var(--tk-slate-200); + -webkit-box-shadow: 0 0 0 1000px var(--tk-slate-800) inset; +} + +/* Scope list on the approval page */ +.tk-list { + margin: 0; + padding: 0; + list-style: none; + text-align: center; + color: var(--tk-slate-200); + font-size: 14px; + line-height: 22px; +} + +.tk-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} diff --git a/testkube/web/static/testkube/wordmark.svg b/testkube/web/static/testkube/wordmark.svg new file mode 100644 index 0000000000..086ab6e5d6 --- /dev/null +++ b/testkube/web/static/testkube/wordmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/testkube/web/templates/approval.html b/testkube/web/templates/approval.html new file mode 100644 index 0000000000..14c1876f3d --- /dev/null +++ b/testkube/web/templates/approval.html @@ -0,0 +1,31 @@ +{{ template "header.html" . }} + +
+
+

Grant access

+ {{ if .Scopes }} +

{{ .Client }} would like to:

+ + {{ else }} +

{{ .Client }} has not requested any personal information.

+ {{ end }} +
+
+
+ + + +
+
+ + + +
+
+
+ +{{ template "footer.html" . }} diff --git a/testkube/web/templates/device.html b/testkube/web/templates/device.html new file mode 100644 index 0000000000..15bc436dc9 --- /dev/null +++ b/testkube/web/templates/device.html @@ -0,0 +1,25 @@ +{{ template "header.html" . }} + +
+
+

Enter user code

+
+ + {{/* The device flow can put the code in the URL. Focus the field when it is empty, + or when the code is not correct. */}} + + +
+
+ +{{ if .Invalid }} + +{{ end }} +
+ +{{ template "footer.html" . }} diff --git a/testkube/web/templates/device_success.html b/testkube/web/templates/device_success.html new file mode 100644 index 0000000000..a9249fd5a9 --- /dev/null +++ b/testkube/web/templates/device_success.html @@ -0,0 +1,10 @@ +{{ template "header.html" . }} + +
+
+

Login successful

+

Return to {{ .ClientName }} to continue.

+
+
+ +{{ template "footer.html" . }} diff --git a/testkube/web/templates/error.html b/testkube/web/templates/error.html new file mode 100644 index 0000000000..7045d830b2 --- /dev/null +++ b/testkube/web/templates/error.html @@ -0,0 +1,37 @@ +{{ template "header.html" . }} + + + +
+
+

Login

+ {{/* frontend.extra.dashboardURL is set per environment (SaaS: app.testkube.io, on-prem: the customer's + dashboard host). The dashboard starts a new login when nobody is logged in, so one button is + enough. Without the value the button returns to the previous page. */}} + {{ $dashboard := extra "dashboardURL" }} +
+ {{ if $dashboard }} + Back to Testkube + {{ else }} + + {{ end }} +
+
+ + +
+ +{{ if not $dashboard }} + +{{ end }} + +{{ template "footer.html" . }} diff --git a/testkube/web/templates/footer.html b/testkube/web/templates/footer.html new file mode 100644 index 0000000000..6a073892d3 --- /dev/null +++ b/testkube/web/templates/footer.html @@ -0,0 +1,5 @@ + + + + + diff --git a/testkube/web/templates/header.html b/testkube/web/templates/header.html new file mode 100644 index 0000000000..356b01c496 --- /dev/null +++ b/testkube/web/templates/header.html @@ -0,0 +1,50 @@ + + + + + Testkube + + + + + + + + + + + + +
+
+
+
+ Testkube +
+{{/* Button icons, from the same antd icon set as the dashboard. Dex loads every file in + templates/ into one template set, so every page can call these partials. */}} +{{- define "tk-icon-github" -}} + +{{- end -}} +{{- define "tk-icon-gitlab" -}} + +{{- end -}} +{{- define "tk-icon-google" -}} + +{{- end -}} +{{- define "tk-icon-lock" -}} + +{{- end -}} +{{/* The lock icon is the default. It covers SSO, OIDC, SAML, LDAP and the local connector. */}} +{{- define "tk-icon-connector" -}} + {{- if eq . "github" }}{{ template "tk-icon-github" }} + {{- else if eq . "gitlab" }}{{ template "tk-icon-gitlab" }} + {{- else if eq . "google" }}{{ template "tk-icon-google" }} + {{- else }}{{ template "tk-icon-lock" }} + {{- end }} +{{- end -}} diff --git a/testkube/web/templates/login.html b/testkube/web/templates/login.html new file mode 100644 index 0000000000..8c55093634 --- /dev/null +++ b/testkube/web/templates/login.html @@ -0,0 +1,16 @@ +{{ template "header.html" . }} + +{{/* Same list as app.testkube.io: "Login via " buttons, one color and icon per provider. */}} +
+

Login

+ +
+ +{{ template "footer.html" . }} diff --git a/testkube/web/templates/oob.html b/testkube/web/templates/oob.html new file mode 100644 index 0000000000..59d0100958 --- /dev/null +++ b/testkube/web/templates/oob.html @@ -0,0 +1,14 @@ +{{ template "header.html" . }} + +
+
+

Login successful

+

Copy this code into your application:

+
+
+ + +
+
+ +{{ template "footer.html" . }} diff --git a/testkube/web/templates/password.html b/testkube/web/templates/password.html new file mode 100644 index 0000000000..4881ad1638 --- /dev/null +++ b/testkube/web/templates/password.html @@ -0,0 +1,85 @@ +{{ template "header.html" . }} + +{{/* Dex sets .UsernamePrompt to "Email Address" for the local connector; LDAP connectors set their own prompt. */}} +{{ $isEmail := eq .UsernamePrompt "Email Address" }} +{{ $fieldLabel := .UsernamePrompt }}{{ if $isEmail }}{{ $fieldLabel = "Email" }}{{ end }} + +
+ + +{{ if .Invalid }} + +{{ end }} +
+ + + +{{/* The antd Input.Password toggle icons. The script shows one icon at a time: + EyeInvisibleOutlined while the password is hidden, EyeOutlined while it is visible. */}} +{{- define "tk-icon-eye-hidden" -}} + +{{- end -}} +{{- define "tk-icon-eye-visible" -}} + +{{- end -}} + +{{ template "footer.html" . }}