Skip to content

Commit

Permalink
feat(auth): 全新 SCOW 登录界面 (#776)
Browse files Browse the repository at this point in the history
# 新登陆界面样式

![image](https://github.com/PKUHPC/SCOW/assets/140392039/134ff1c0-a0dd-4ad3-9736-574a5272ed1b)
增加大背景图,并支持在  `dev/vagrant/config/auth.yaml` 修改背景图和背景色

![image](https://github.com/PKUHPC/SCOW/assets/140392039/aede6854-1eae-4ca7-a037-25bdedde4efd)

## 登录框
### 旧

![image](https://github.com/PKUHPC/SCOW/assets/140392039/58b3834b-14d9-4f0e-a86d-9e3615ed94c4)
### 新

![image](https://github.com/PKUHPC/SCOW/assets/140392039/e7bdbe79-c6ab-47c7-8c94-5e63bf1f7c97)
### 修改内容
1. 登录按钮大圆角改为小圆角
2. 密码框新增点击切换显示/隐藏密码

## 图标
### 旧版图标显示在登录框中

![image](https://github.com/PKUHPC/SCOW/assets/140392039/8be1fae3-d60b-44a2-b3d4-579014739fd2)
### 新版显示在页面左上角

![image](https://github.com/PKUHPC/SCOW/assets/140392039/1d0578e9-21b1-4164-b7ee-56262a684190)
并且新版支持在配置文件 `dev/vagrant/config/auth.yaml` 中修改图标为彩色或是白色

![image](https://github.com/PKUHPC/SCOW/assets/140392039/98cb2baa-3d83-4933-a454-f60f5bcf7c82)


## Powered By
### 旧版无 Powered By
### 新版显示在页面页面左下角

![image](https://github.com/PKUHPC/SCOW/assets/140392039/d52efcb1-c901-4bd1-a749-f07a6d14a741)

## slogan
### 旧版无 slogan
### 新版 slogan

![image](https://github.com/PKUHPC/SCOW/assets/140392039/19b18b98-eb2e-43b4-88ce-eac1a05dc348)

支持在  `dev/vagrant/config/auth.yaml` 修改 slogan 字体颜色和具体的内容

![image](https://github.com/PKUHPC/SCOW/assets/140392039/6ef3743c-9197-4bba-9fa3-d941ba9a8075)
  • Loading branch information
Miracle575 authored Aug 10, 2023
1 parent 9edc869 commit b2a52c5
Show file tree
Hide file tree
Showing 14 changed files with 239 additions and 88 deletions.
6 changes: 6 additions & 0 deletions .changeset/warm-starfishes-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@scow/auth": patch
"@scow/docs": patch
---

全新 SCOW 登录界面
23 changes: 23 additions & 0 deletions apps/auth/config/auth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,26 @@ ldap:

ssh:
baseNode: localhost:22222

# auth 界面 ui 配置
ui:
# 登录界面背景图
backgroundImagePath: "./assets/background.png"
# 登录界面背景色,当背景图无法加载时,背景色起效
backgroundFallbackColor: "#9a0000"
# 登录界面 logo 图, light: 亮色模式下的 logo, dark: 黑暗模式下的 logo
logoType: "dark"

# 登录界面 slogan 配置
slogan:
# 登录界面 slogan 文字颜色
color: "white"
# 登录界面 slogan title
title: "开源算力中心门户和管理平台"
# 多条 slogan 文本
texts:
- "图形化界面,使用方便"
- "功能丰富,管理简单"
- "一体化部署,开箱即用"
- "标准化平台,支持算力融合"
- "开源中立,独立自主"
2 changes: 1 addition & 1 deletion apps/auth/config/ui.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
footer:
defaultText: footer default text
defaultText: footer default text
Binary file added apps/auth/public/background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/auth/public/icons/eye-close.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/auth/public/icons/eye.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 10 additions & 3 deletions apps/auth/src/auth/loginHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
* See the Mulan PSL v2 for more details.
*/

import { DEFAULT_PRIMARY_COLOR } from "@scow/config/build/ui";
import { FastifyReply, FastifyRequest } from "fastify";
import { join } from "path";
import { createCaptcha } from "src/auth/captcha";
Expand Down Expand Up @@ -42,6 +41,7 @@ export async function serveLoginHtml(
const hostname = parseHostname(req);
const enableCaptcha = authConfig.captcha.enabled;
const enableTotp = authConfig.otp?.enabled;
const logoPreferDarkParam = authConfig.ui?.logoType === "light" ? "false" : "true";

// 显示绑定otp按钮:otp.enabled为true && (密钥存于ldap时 || (otp.type===remote && 配置了otp.remote.redirectUrl))
const showBindOtpButton = authConfig.otp?.enabled && (authConfig.otp?.type === OtpStatusOptions.ldap
Expand All @@ -53,10 +53,17 @@ export async function serveLoginHtml(
return rep.status(
verifyCaptchaFail ? 400 : err ? 401 : 200).view("login.liquid", {
cssUrl: join(config.BASE_PATH, config.AUTH_BASE_PATH, "/public/assets/tailwind.min.css"),
eyeImagePath: join(config.BASE_PATH, config.AUTH_BASE_PATH, "/public/assets/icons/eye.png"),
eyeCloseImagePath: join(config.BASE_PATH, config.AUTH_BASE_PATH, "/public/assets/icons/eye-close.png"),
backgroundImagePath: join(config.BASE_PATH, config.PUBLIC_PATH,
authConfig.ui?.backgroundImagePath ?? "./assets/background.png"),
backgroundFallbackColor: authConfig.ui?.backgroundFallbackColor || "#9a0000",
faviconUrl: join(config.BASE_PATH, FAVICON_URL),
logoUrl: join(config.BASE_PATH, LOGO_URL),
backgroundColor: uiConfig.primaryColor?.defaultColor ?? DEFAULT_PRIMARY_COLOR,
logoUrl: join(config.BASE_PATH, LOGO_URL + logoPreferDarkParam),
callbackUrl,
sloganColor: authConfig.ui?.slogan.color || "white",
sloganTitle: authConfig.ui?.slogan.title || "",
sloganTextArr: authConfig.ui?.slogan.texts || [],
footerText: (hostname && uiConfig?.footer?.hostnameTextMap?.[hostname]) ?? uiConfig?.footer?.defaultText ?? "",
err,
...captchaInfo,
Expand Down
13 changes: 13 additions & 0 deletions apps/auth/src/config/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,17 @@ export const OtpLdapSchema = Type.Object({
}, { description: "发送绑定链接相关配置" }),
}, { description: "将otp密钥存在ldap需要配置信息" });

export const UiConfigSchema = Type.Object({
backgroundImagePath: Type.String({ description: "默认背景图片", default: "./assets/background.png" }),
backgroundFallbackColor: Type.String({ description: "默认背景颜色", default: "#9a0000" }),
logoType: Type.String({ description: "默认图标类型", default: "dark" }),
slogan: Type.Object({
color: Type.String({ description: "默认标语文字颜色", default: "white" }),
title: Type.String({ description: "默认标语标题", default: "" }),
texts: Type.Array(Type.String(), { description: "默认 slogan 正文数组", default: []}),
}, { default: {} }),
});

export const OtpConfigSchema = Type.Object({
enabled: Type.Boolean({ description: "是否启用otp", default: false }),
type: Type.Optional(Type.Enum(OtpStatusOptions, { description: "otp功能状态" })),
Expand All @@ -164,6 +175,7 @@ export const OtpConfigSchema = Type.Object({
export type SshConfigSchema = Static<typeof SshConfigSchema>;
export type OtpLdapSchema = Static<typeof OtpLdapSchema>;
export type OtpConfigSchema = Static<typeof OtpConfigSchema>;
export type UiConfigSchema = Static<typeof UiConfigSchema>;

export const AuthConfigSchema = Type.Object({
redisUrl: Type.String({ description: "存放token的redis地址", default: "redis:6379" }),
Expand All @@ -180,6 +192,7 @@ export const AuthConfigSchema = Type.Object({
enabled: Type.Boolean({ description: "验证码功能是否启用", default: false }),
}, { default: {} }),
otp: Type.Optional(OtpConfigSchema),
ui: Type.Optional(UiConfigSchema),
});

export type AuthConfigSchema = Static<typeof AuthConfigSchema>;
Expand Down
3 changes: 2 additions & 1 deletion apps/auth/src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { AuthType } from "./AuthType";

export const FAVICON_URL = "/api/icon?type=favicon";

export const LOGO_URL = "/api/logo?type=logo&preferDark=false";
export const LOGO_URL = "/api/logo?type=logo&preferDark=";

export const config = envConfig({
HOST: host({ default: "0.0.0.0", desc: "监听地址" }),
Expand All @@ -45,6 +45,7 @@ export const config = envConfig({

EXTRA_ALLOWED_CALLBACK_HOSTNAMES: str({ desc: "额外的信任回调域名,以逗号分隔", default: "" }),

PUBLIC_PATH: str({ desc: "静态文件路径前缀。以/开头,以/结尾", default: "/__public__/" }),
});

export const rootKeyPair = getKeyPair(config.SSH_PRIVATE_KEY_PATH, config.SSH_PUBLIC_KEY_PATH);
201 changes: 120 additions & 81 deletions apps/auth/views/login.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -2,112 +2,151 @@
<html>

<head>
<title>登录</title>
<link href="{{ cssUrl }}" rel="stylesheet">
<link rel="icon" type="image/x-icon" href="{{ faviconUrl }}"></link>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body{
background: linear-gradient(#fafaff, #d8daea);
}
.button-primary {
background-color: {{ backgroundColor }}
}
svg{
height: 100%;
width: 100%;
}
</style>
<title>登录</title>
<link href="{{ cssUrl }}" rel="stylesheet">
<link rel="icon" type="image/x-icon" href="{{ faviconUrl }}"></link>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html {
min-width: max-content;
overflow-x: auto;
overflow-y: hidden;
}
body {
background-image: url("{{ backgroundImagePath }}");
background-repeat: no-repeat;
background-color: {{ backgroundFallbackColor }};
background-size: cover;
}
.button-primary {
background-color: #9a0000;
svg {
height: 100%;
width: 100%;
}
</style>
</head>

<body style="font-family:Roboto">
<div class="w-full h-screen flex items-center justify-center">
<div class="w-full md:w-1/3 max-w-lg bg-white rounded-lg py-12">
<form method="post" action="">
<img class="m-auto pb-12 w-1/2" src="{{ logoUrl }}" alt="登录"/>

<div class="px-12">
<div class="w-full mb-4">
<div class="flex items-center">
<input type='text' name="username" placeholder="用户名" required
class="px-8 w-full border rounded px-3 py-2 text-gray-700 focus:outline-none" />
</div>
</div>
<div class="w-full mb-4">
<div class="flex items-center">
<input name="password" placeholder="密码" type="password" required
class="px-8 w-full border rounded px-3 py-2 text-gray-700 focus:outline-none" />
</div>
<div class="fixed top-6 left-24">
<a href="https://icode.pku.edu.cn/SCOW/">
<img class="w-40" src="{{ logoUrl }}" alt="logo">
</a>
</div>
<div class="w-full h-screen flex">
<div class="w-1/2 h-screen ml-20 flex items-center justify-center">
<div class="w-80 max-w-md min-w-max bg-white rounded-lg py-12">
<form method="post" action="">
<div class="mb-16 text-2xl font-semibold text-center">账号密码登录</div>
<div class="px-14 flex flex-col items-center">
<div class="w-72 mb-10">
<input type='text' name="username" placeholder="用户名" required
class="px-8 w-full border rounded px-3 py-2 text-gray-700 focus:outline-none" />
</div>
<div class="w-72 mb-10">
<div class="relative flex items-center">
<input id="password" name="password" placeholder="密码" type="password" required
class="px-8 w-full border rounded px-3 py-2 text-gray-700 focus:outline-none" />
<div id="eye-elem" class="absolute w-5 h-5 right-3 bg-contain"></div>
</div>
{% if enableTotp %}
<div class="w-full mb-4">
</div>
{% if enableTotp %}
<div class="w-full mb-10">
<div class="flex items-center">
<input name="otpCode" placeholder="OTP验证码" type="text" required
class="px-8 w-full py-2 border rounded text-gray-700 focus:outline-none"/>
</div>
</div>
{% endif %}
{% if enableCaptcha %}
<div class="w-full mb-4">
<div class="flex items-center">
<input name="code" placeholder="请输入验证码" type="text" required
class=" px-8 w-full border rounded px-3 py-2 text-gray-700 focus:outline-none" />
<div id="captcha" onclick="refreshCaptcha()" class="cursor-pointer">{{ code }}</div>
<script>
function refreshCaptcha(){
const captchaDiv = document.getElementById("captcha");
fetch("{{ refreshCaptchaPath }}", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token: "{{ token }}",
}),}
).then( async function (response) {
captchaDiv.innerHTML = await response.text();
}).catch(() => {
captchaDiv.textContent = "刷新失败,请点击重试"
});
}
</script>
</div>
{% endif %}
{% if enableCaptcha %}
<div class="w-full mb-10">
<div class="flex items-center">
<input name="code" placeholder="请输入验证码" type="text" required
class=" px-8 w-full border rounded px-3 py-2 text-gray-700 focus:outline-none" />
<div id="captcha" onclick="refreshCaptcha()" class="cursor-pointer">{{ code }}</div>
<script>
function refreshCaptcha(){
const captchaDiv = document.getElementById("captcha");
fetch("{{ refreshCaptchaPath }}", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token: "{{ token }}",
}),}
).then( async function (response) {
captchaDiv.innerHTML = await response.text();
}).catch(() => {
captchaDiv.textContent = "刷新失败,请点击重试"
});
}
</script>
</div>
</div>

{% if verifyCaptchaFail %}
<p class="my-4 text-center text-red-600">验证码无效,请重新输入。</p>
{% endif %}

{% else %}
{% else %}
<input type="hidden" name="code" value="" />
{% endif %}
{% endif %}

<input type="hidden" name="token" value="{{ token }}" />
<input type="hidden" name="callbackUrl" value="{{ callbackUrl }}" />
<input type="hidden" name="token" value="{{ token }}" />
<input type="hidden" name="callbackUrl" value="{{ callbackUrl }}" />

{% if err %}
{% if err %}
<p class="my-4 text-center text-red-600">用户名/密码无效,请检查。</p>
{% endif %}
{% if verifyOtpFail %}
{% endif %}
{% if verifyOtpFail %}
<p class="my-4 text-center text-red-600">OTP验证码无效,请重新输入。</p>
{% endif %}
<button type="submit" class="w-full mt-4 py-2 rounded-full button-primary text-gray-100 focus:outline-none">
登录
</button>
{% endif %}
<button type="submit" class="w-72 py-2 mb-14 rounded button-primary text-gray-100 focus:outline-none">
登录
</button>
</div>
</form>
{% if showBindOtpButton %}
<div class="px-12 mt-4">
<form action="{{ otpBasePath }}/bind" method="get">
<button type="submit" name="action" value="bindOtp" class="px text-gray-400">
绑定otp
</button>
<input type="hidden" name="callbackUrl" value="{{ callbackUrl }}" />
</form>
</div>
{% endif %}
<div class="px-12 mt-4">
<form action="{{ otpBasePath }}/bind" method="get">
<button type="submit" name="action" value="bindOtp" class="px text-gray-400">
绑定otp
</button>
<input type="hidden" name="callbackUrl" value="{{ callbackUrl }}" />
</form>
</div>
{% endif %}
</div>
</div>
<p class="absolute bottom-0 w-full text-center my-4 text-gray-500 text-sm">
{{ footerText }}
<div class="w-1/2 h-screen pl-48 pb-28 flex justify-center flex-col">
<div class="text-4xl font-semibold mb-12" style="color: {{ sloganColor }}">{{ sloganTitle }}</div>
{% for sloganText in sloganTextArr %}
<div class="text-2xl mb-8" style="color: {{ sloganColor }}">{{ sloganText }}</div>
{% endfor %}
</div>
</div>
<p class="absolute bottom-0 w-full pl-24 my-4 text-white text-xs">
Powered by SCOW
</p>
<script>
// 图片预加载
const eyeImg = new Image();
eyeImg.src = "{{ eyeImagePath }}";
const eyeCloseImg = new Image();
eyeCloseImg.src = "{{ eyeCloseImagePath }}";
const passwordInput = document.getElementById('password');
const eyeElem = document.getElementById('eye-elem');
eyeElem.style.backgroundImage = "url('" + eyeCloseImg.src + "')"
eyeElem.style.cursor = "pointer"
eyeElem.addEventListener("click", function() {
if (passwordInput.type === "password") {
passwordInput.setAttribute("type", "text");
eyeElem.style.backgroundImage = "url('" + eyeImg.src + "')"
} else {
passwordInput.setAttribute("type", "password");
eyeElem.style.backgroundImage = "url('" + eyeCloseImg.src + "')"
}
})
</script>
</body>
</html>
23 changes: 23 additions & 0 deletions dev/vagrant/config/auth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,26 @@ ldap:
# 例如:sn: "{{ cn }}",那么添加时将会增加一个sn属性,其值为cn的属性,即为用户输入的姓名
# extraProps:
# key: value

# auth 界面 ui 配置
ui:
# 登录界面背景图
backgroundImagePath: "./assets/background.png"
# 登录界面背景色,当背景图无法加载时,背景色起效
backgroundFallbackColor: "#9a0000"
# 登录界面 logo 图, light: 亮色模式下的 logo, dark: 黑暗模式下的 logo
logoType: "dark"

# 登录界面 slogan 配置
slogan:
# 登录界面 slogan 文字颜色
color: "white"
# 登录界面 slogan title
title: "开源算力中心门户和管理平台"
# 多条 slogan 文本
texts:
- "图形化界面,使用方便"
- "功能丰富,管理简单"
- "一体化部署,开箱即用"
- "标准化平台,支持算力融合"
- "开源中立,独立自主"
1 change: 1 addition & 0 deletions dev/vagrant/config/ui.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ primaryColor:
# hostnameTextMap:
# 从a.com的访问的主题色为#000000
# a.com: "#000000"

Loading

0 comments on commit b2a52c5

Please sign in to comment.