mirror of
https://github.com/coaidev/coai.git
synced 2025-05-19 04:50:14 +09:00
feat: add custom website title and logo feature; support update root password in admin system settings
This commit is contained in:
parent
89e5e13f97
commit
4efd0e8928
@ -29,6 +29,10 @@ type SubscriptionOperationForm struct {
|
||||
Month int64 `json:"month"`
|
||||
}
|
||||
|
||||
type UpdateRootPasswordForm struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func InfoAPI(c *gin.Context) {
|
||||
db := utils.GetDBFromContext(c)
|
||||
cache := utils.GetCacheFromContext(c)
|
||||
@ -161,3 +165,29 @@ func UserSubscriptionAPI(c *gin.Context) {
|
||||
"status": true,
|
||||
})
|
||||
}
|
||||
|
||||
func UpdateRootPasswordAPI(c *gin.Context) {
|
||||
var form UpdateRootPasswordForm
|
||||
if err := c.ShouldBindJSON(&form); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
db := utils.GetDBFromContext(c)
|
||||
cache := utils.GetCacheFromContext(c)
|
||||
err := UpdateRootPassword(db, cache, form.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": true,
|
||||
})
|
||||
}
|
||||
|
@ -23,4 +23,5 @@ func Register(app *gin.RouterGroup) {
|
||||
app.GET("/admin/user/list", UserPaginationAPI)
|
||||
app.POST("/admin/user/quota", UserQuotaAPI)
|
||||
app.POST("/admin/user/subscription", UserSubscriptionAPI)
|
||||
app.POST("/admin/user/root", UpdateRootPasswordAPI)
|
||||
}
|
||||
|
@ -2,8 +2,12 @@ package admin
|
||||
|
||||
import (
|
||||
"chat/utils"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -109,3 +113,19 @@ func SubscriptionOperation(db *sql.DB, id int64, month int64) error {
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateRootPassword(db *sql.DB, cache *redis.Client, password string) error {
|
||||
password = strings.TrimSpace(password)
|
||||
if len(password) < 6 || len(password) > 36 {
|
||||
return fmt.Errorf("password length must be between 6 and 36")
|
||||
}
|
||||
|
||||
if _, err := db.Exec(`
|
||||
UPDATE auth SET password = ? WHERE username = 'root'
|
||||
`, utils.Sha2Encrypt(password)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cache.Del(context.Background(), fmt.Sprint("nio:user:root"))
|
||||
return nil
|
||||
}
|
||||
|
24
app/src/admin/api/info.ts
Normal file
24
app/src/admin/api/info.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from "axios";
|
||||
import { setAppLogo, setAppName } from "@/utils/env.ts";
|
||||
|
||||
export type SiteInfo = {
|
||||
title: string;
|
||||
logo: string;
|
||||
};
|
||||
|
||||
export async function getSiteInfo(): Promise<SiteInfo> {
|
||||
try {
|
||||
const response = await axios.get("/info");
|
||||
return response.data as SiteInfo;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return { title: "", logo: "" };
|
||||
}
|
||||
}
|
||||
|
||||
export function syncSiteInfo() {
|
||||
getSiteInfo().then((info) => {
|
||||
setAppName(info.title);
|
||||
setAppLogo(info.logo);
|
||||
});
|
||||
}
|
@ -3,6 +3,8 @@ import { getErrorMessage } from "@/utils/base.ts";
|
||||
import axios from "axios";
|
||||
|
||||
export type GeneralState = {
|
||||
title: string;
|
||||
logo: string;
|
||||
backend: string;
|
||||
};
|
||||
|
||||
@ -47,8 +49,21 @@ export async function setConfig(config: SystemProps): Promise<CommonResponse> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateRootPassword(
|
||||
password: string,
|
||||
): Promise<CommonResponse> {
|
||||
try {
|
||||
const response = await axios.post(`/admin/user/root`, { password });
|
||||
return response.data as CommonResponse;
|
||||
} catch (e) {
|
||||
return { status: false, error: getErrorMessage(e) };
|
||||
}
|
||||
}
|
||||
|
||||
export const initialSystemState: SystemProps = {
|
||||
general: {
|
||||
logo: "",
|
||||
title: "",
|
||||
backend: "",
|
||||
},
|
||||
mail: {
|
||||
|
@ -15,6 +15,7 @@
|
||||
.logo {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
& > * {
|
||||
|
@ -31,6 +31,7 @@
|
||||
margin: 2px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
@ -234,13 +234,18 @@ input[type="number"] {
|
||||
}
|
||||
}
|
||||
|
||||
.paragraph-space {
|
||||
width: 100%;
|
||||
height: 0.25rem;
|
||||
}
|
||||
|
||||
.paragraph-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 1rem;
|
||||
|
||||
& > * {
|
||||
margin-right: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
|
@ -78,6 +78,10 @@ export function ParagraphDescription({ children }: { children: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ParagraphSpace() {
|
||||
return <div className={`paragraph-space`} />;
|
||||
}
|
||||
|
||||
function ParagraphFooter({ children }: { children: React.ReactNode }) {
|
||||
return <div className={`paragraph-footer`}>{children}</div>;
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import MenuBar from "./MenuBar.tsx";
|
||||
import { getMemory } from "@/utils/memory.ts";
|
||||
import { goAuth } from "@/utils/app.ts";
|
||||
import Avatar from "@/components/Avatar.tsx";
|
||||
import { appLogo } from "@/utils/env.ts";
|
||||
|
||||
function NavMenu() {
|
||||
const username = useSelector(selectUsername);
|
||||
@ -53,7 +54,7 @@ function NavBar() {
|
||||
</Button>
|
||||
<img
|
||||
className={`logo`}
|
||||
src="/favicon.ico"
|
||||
src={appLogo}
|
||||
alt=""
|
||||
onClick={() => router.navigate("/")}
|
||||
/>
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
import { getMemory } from "@/utils/memory.ts";
|
||||
import { Compass, Image, Newspaper } from "lucide-react";
|
||||
import React from "react";
|
||||
import { syncSiteInfo } from "@/admin/api/info.ts";
|
||||
|
||||
export const version = "3.8.0";
|
||||
export const dev: boolean = getDev();
|
||||
@ -524,3 +525,5 @@ export function login() {
|
||||
axios.defaults.baseURL = rest_api;
|
||||
axios.defaults.headers.post["Content-Type"] = "application/json";
|
||||
axios.defaults.headers.common["Authorization"] = getMemory(tokenField);
|
||||
|
||||
syncSiteInfo();
|
||||
|
@ -464,9 +464,18 @@
|
||||
"search": "联网搜索",
|
||||
"mail": "SMTP 发件设置",
|
||||
"save": "保存",
|
||||
"updateRoot": "修改 Root 密码",
|
||||
"updateRootTip": "请谨慎操作,修改 Root 密码后,您需要重新登录。",
|
||||
"updateRootPlaceholder": "请输入新的 Root 密码",
|
||||
"updateRootRepeatPlaceholder": "请再次输入新的 Root 密码",
|
||||
"test": "测试发件",
|
||||
"title": "网站名称",
|
||||
"titleTip": "网站名称,用于显示在网站标题,留空默认",
|
||||
"logo": "网站 Logo",
|
||||
"logoTip": "网站 Logo 的链接,用于显示在网站标题,留空默认 (如 {{logo}})",
|
||||
"backend": "后端域名",
|
||||
"backendTip": "后端回调域名(docker 安装默认路径为 /api),接收回调参数。",
|
||||
"backendPlaceholder": "后端回调域名,默认为空,接受回调必填(如 {{backend}})",
|
||||
"mailHost": "发件域名",
|
||||
"mailPort": "SMTP 端口",
|
||||
"mailUser": "用户名",
|
||||
@ -474,7 +483,7 @@
|
||||
"mailFrom": "发件人",
|
||||
"searchEndpoint": "搜索接入点",
|
||||
"searchQuery": "最大搜索结果数",
|
||||
"searchTip": "DuckDuckGo 搜索接入点,如不填写自动使用 WebPilot 和 New Bing 逆向进行搜索功能。\nDuckDuckGo API 项目搭建:[duckduckgo-api](https://github.com/binjie09/duckduckgo-api)。"
|
||||
"searchTip": "DuckDuckGo 搜索接入点,如不填写自动使用 WebPilot 和 New Bing 逆向进行搜索功能(速度较慢)。\nDuckDuckGo API 项目搭建:[duckduckgo-api](https://github.com/binjie09/duckduckgo-api)。"
|
||||
}
|
||||
},
|
||||
"mask": {
|
||||
|
@ -425,7 +425,16 @@
|
||||
"searchQuery": "Max Search Results",
|
||||
"searchTip": "DuckDuckGo search endpoint, if not filled in, use WebPilot and New Bing reverse search function by default.\nDuckDuckGo API project build: [duckduckgo-api](https://github.com/binjie09/duckduckgo-api).",
|
||||
"mailFrom": "Sender",
|
||||
"test": "Test outgoing"
|
||||
"test": "Test outgoing",
|
||||
"updateRoot": "Change Root Password",
|
||||
"updateRootTip": "Please proceed with caution, after changing the root password, you will need to log in again.",
|
||||
"updateRootPlaceholder": "Please enter a new root password",
|
||||
"updateRootRepeatPlaceholder": "Please enter the new root password again",
|
||||
"title": "Site name",
|
||||
"titleTip": "Site name to display in the site title, leave blank for default",
|
||||
"logo": "Site Logo",
|
||||
"logoTip": "Link to the site logo to display in the site title, leave blank for default (e.g. {{logo}})",
|
||||
"backendPlaceholder": "Backend callback domain name, empty by default, required for accepting callbacks (e.g. {{backend}})"
|
||||
},
|
||||
"user": "User Management",
|
||||
"invitation-code": "Invitation Code",
|
||||
|
@ -425,7 +425,16 @@
|
||||
"searchQuery": "検索結果の最大数",
|
||||
"searchTip": "DuckDuckGoは、入力せずにWebPilotやNew Bing Reverse Searchなどのアクセスポイントを自動的に検索します。\\ nDuckDuckGo APIプロジェクトビルド:[ duckduckgo - api ]( https://github.com/binjie09/duckduckgo-api )。",
|
||||
"mailFrom": "発信元",
|
||||
"test": "テスト送信"
|
||||
"test": "テスト送信",
|
||||
"updateRoot": "ルートパスワードの変更",
|
||||
"updateRootTip": "ルートパスワードを変更した後、再度ログインする必要がありますので、慎重に進んでください。",
|
||||
"updateRootPlaceholder": "新しいrootパスワードを入力してください",
|
||||
"updateRootRepeatPlaceholder": "新しいrootパスワードをもう一度入力してください",
|
||||
"title": "サイト名",
|
||||
"titleTip": "サイトタイトルに表示するサイト名、デフォルトの場合は空白のままにします",
|
||||
"logo": "サイトロゴ",
|
||||
"logoTip": "サイトタイトルに表示するサイトロゴへのリンク、デフォルトの場合は空白のままにします(例:{{ logo }})",
|
||||
"backendPlaceholder": "バックエンドコールバックドメイン名、デフォルトで空、コールバックを受け入れるために必要(例:{{ backend }})"
|
||||
},
|
||||
"user": "ユーザー管理",
|
||||
"invitation-code": "招待コード",
|
||||
|
@ -425,7 +425,16 @@
|
||||
"searchQuery": "Максимальное количество результатов поиска",
|
||||
"searchTip": "Конечная точка поиска DuckDuckGo, если она не заполнена, по умолчанию используется функция обратного поиска WebPilot и New Bing.\nСборка проекта DuckDuckGo API: [duckduckgo-api](https://github.com/binjie09/duckduckgo-api).",
|
||||
"mailFrom": "От",
|
||||
"test": "Тест исходящий"
|
||||
"test": "Тест исходящий",
|
||||
"updateRoot": "Изменить корневой пароль",
|
||||
"updateRootTip": "Пожалуйста, соблюдайте осторожность, после смены пароля root вам нужно будет снова войти в систему.",
|
||||
"updateRootPlaceholder": "Введите новый пароль root",
|
||||
"updateRootRepeatPlaceholder": "Введите новый пароль root еще раз",
|
||||
"title": "Название сайта",
|
||||
"titleTip": "Название сайта для отображения в заголовке сайта, оставьте пустым по умолчанию",
|
||||
"logo": "Логотип сайта",
|
||||
"logoTip": "Ссылка на логотип сайта для отображения в заголовке сайта, оставьте поле пустым по умолчанию (например, {{logo}})",
|
||||
"backendPlaceholder": "Имя домена обратного вызова Backend, пустое по умолчанию, требуется для приема обратных вызовов (например, {{backend}})"
|
||||
},
|
||||
"user": "Управление пользователями",
|
||||
"invitation-code": "Код приглашения",
|
||||
|
@ -10,7 +10,7 @@ import router from "@/router.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getQueryParam } from "@/utils/path.ts";
|
||||
import { setMemory } from "@/utils/memory.ts";
|
||||
import { useDeeptrain } from "@/utils/env.ts";
|
||||
import { appLogo, appName, useDeeptrain } from "@/utils/env.ts";
|
||||
import { Card, CardContent } from "@/components/ui/card.tsx";
|
||||
import { goAuth } from "@/utils/app.ts";
|
||||
import { Label } from "@/components/ui/label.tsx";
|
||||
@ -132,8 +132,10 @@ function Login() {
|
||||
|
||||
return (
|
||||
<div className={`auth-container`}>
|
||||
<img className={`logo`} src="/favicon.ico" alt="" />
|
||||
<div className={`title`}>{t("login")} Chat Nio</div>
|
||||
<img className={`logo`} src={appLogo} alt="" />
|
||||
<div className={`title`}>
|
||||
{t("login")} {appName}
|
||||
</div>
|
||||
<Card className={`auth-card`}>
|
||||
<CardContent className={`pb-0`}>
|
||||
<div className={`auth-wrapper`}>
|
||||
|
@ -14,6 +14,7 @@ import Require, {
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import TickButton from "@/components/TickButton.tsx";
|
||||
import { appLogo } from "@/utils/env.ts";
|
||||
|
||||
function Forgot() {
|
||||
const { t } = useTranslation();
|
||||
@ -57,7 +58,7 @@ function Forgot() {
|
||||
|
||||
return (
|
||||
<div className={`auth-container`}>
|
||||
<img className={`logo`} src="/favicon.ico" alt="" />
|
||||
<img className={`logo`} src={appLogo} alt="" />
|
||||
<div className={`title`}>{t("auth.reset-password")}</div>
|
||||
<Card className={`auth-card`}>
|
||||
<CardContent className={`pb-0`}>
|
||||
|
@ -12,6 +12,7 @@ import { useToast } from "@/components/ui/use-toast.ts";
|
||||
import { handleGenerationData } from "@/utils/processor.ts";
|
||||
import { selectModel } from "@/store/chat.ts";
|
||||
import ModelFinder from "@/components/home/ModelFinder.tsx";
|
||||
import { appLogo } from "@/utils/env.ts";
|
||||
|
||||
type WrapperProps = {
|
||||
onSend?: (value: string, model: string) => boolean;
|
||||
@ -127,7 +128,7 @@ function Wrapper({ onSend }: WrapperProps) {
|
||||
</div>
|
||||
) : (
|
||||
<div className={`product`}>
|
||||
<img src={`/favicon.ico`} alt={""} />
|
||||
<img src={appLogo} alt={""} />
|
||||
AI Code Generator
|
||||
</div>
|
||||
)}
|
||||
|
@ -16,6 +16,7 @@ import { useToast } from "@/components/ui/use-toast.ts";
|
||||
import TickButton from "@/components/TickButton.tsx";
|
||||
import { validateToken } from "@/store/auth.ts";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { appLogo, appName } from "@/utils/env.ts";
|
||||
|
||||
type CompProps = {
|
||||
form: RegisterForm;
|
||||
@ -225,8 +226,10 @@ function Register() {
|
||||
|
||||
return (
|
||||
<div className={`auth-container`}>
|
||||
<img className={`logo`} src="/favicon.ico" alt="" />
|
||||
<div className={`title`}>{t("register")} Chat Nio</div>
|
||||
<img className={`logo`} src={appLogo} alt="" />
|
||||
<div className={`title`}>
|
||||
{t("register")} {appName}
|
||||
</div>
|
||||
<Card className={`auth-card`}>
|
||||
<CardContent className={`pb-0`}>
|
||||
{!next ? (
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
SearchState,
|
||||
setConfig,
|
||||
SystemProps,
|
||||
updateRootPassword,
|
||||
} from "@/admin/api/system.ts";
|
||||
import { useEffectAsync } from "@/utils/hook.ts";
|
||||
import { toastState } from "@/admin/utils.ts";
|
||||
@ -38,6 +39,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog.tsx";
|
||||
import { DialogTitle } from "@radix-ui/react-dialog";
|
||||
import Require from "@/components/Require.tsx";
|
||||
|
||||
type CompProps<T> = {
|
||||
data: T;
|
||||
@ -45,6 +47,83 @@ type CompProps<T> = {
|
||||
onChange: (doToast?: boolean) => Promise<void>;
|
||||
};
|
||||
|
||||
function RootDialog() {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [repeat, setRepeat] = useState<string>("");
|
||||
|
||||
const onPost = async () => {
|
||||
const res = await updateRootPassword(password);
|
||||
toastState(toast, t, res, true);
|
||||
if (res.status) {
|
||||
setPassword("");
|
||||
setRepeat("");
|
||||
setOpen(false);
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant={`outline`} size={`sm`}>
|
||||
{t("admin.system.updateRoot")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("admin.system.updateRoot")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div className={`mb-4 select-none`}>
|
||||
{t("admin.system.updateRootTip")}
|
||||
</div>
|
||||
<Input
|
||||
className={`mb-2`}
|
||||
type={`password`}
|
||||
placeholder={t("admin.system.updateRootPlaceholder")}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
type={`password`}
|
||||
placeholder={t("admin.system.updateRootRepeatPlaceholder")}
|
||||
value={repeat}
|
||||
onChange={(e) => setRepeat(e.target.value)}
|
||||
/>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant={`outline`}
|
||||
onClick={() => {
|
||||
setPassword("");
|
||||
setRepeat("");
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("admin.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={`default`}
|
||||
loading={true}
|
||||
onClick={onPost}
|
||||
disabled={
|
||||
password.trim().length === 0 || password.trim() !== repeat.trim()
|
||||
}
|
||||
>
|
||||
{t("admin.confirm")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function General({ data, dispatch, onChange }: CompProps<GeneralState>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -55,7 +134,37 @@ function General({ data, dispatch, onChange }: CompProps<GeneralState>) {
|
||||
isCollapsed={true}
|
||||
>
|
||||
<ParagraphItem>
|
||||
<Label>{t("admin.system.backend")}</Label>
|
||||
<Label>{t("admin.system.title")}</Label>
|
||||
<Input
|
||||
value={data.title}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
type: "update:general.title",
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t("admin.system.titleTip")}
|
||||
/>
|
||||
</ParagraphItem>
|
||||
<ParagraphItem>
|
||||
<Label>{t("admin.system.logo")}</Label>
|
||||
<Input
|
||||
value={data.logo}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
type: "update:general.logo",
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t("admin.system.logoTip", {
|
||||
logo: `${window.location.protocol}//${window.location.host}/favicon.ico`,
|
||||
})}
|
||||
/>
|
||||
</ParagraphItem>
|
||||
<ParagraphItem>
|
||||
<Label>
|
||||
<Require /> {t("admin.system.backend")}
|
||||
</Label>
|
||||
<Input
|
||||
value={data.backend}
|
||||
onChange={(e) =>
|
||||
@ -64,7 +173,9 @@ function General({ data, dispatch, onChange }: CompProps<GeneralState>) {
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={`${window.location.protocol}//${window.location.host}/api`}
|
||||
placeholder={t("admin.system.backendPlaceholder", {
|
||||
backend: `${window.location.protocol}//${window.location.host}/api`,
|
||||
})}
|
||||
/>
|
||||
</ParagraphItem>
|
||||
<ParagraphDescription>
|
||||
@ -72,7 +183,12 @@ function General({ data, dispatch, onChange }: CompProps<GeneralState>) {
|
||||
</ParagraphDescription>
|
||||
<ParagraphFooter>
|
||||
<div className={`grow`} />
|
||||
<Button size={`sm`} loading={true} onClick={async () => await onChange()}>
|
||||
<RootDialog />
|
||||
<Button
|
||||
size={`sm`}
|
||||
loading={true}
|
||||
onClick={async () => await onChange()}
|
||||
>
|
||||
{t("admin.system.save")}
|
||||
</Button>
|
||||
</ParagraphFooter>
|
||||
@ -102,7 +218,9 @@ function Mail({ data, dispatch, onChange }: CompProps<MailState>) {
|
||||
isCollapsed={true}
|
||||
>
|
||||
<ParagraphItem>
|
||||
<Label>{t("admin.system.mailHost")}</Label>
|
||||
<Label>
|
||||
<Require /> {t("admin.system.mailHost")}
|
||||
</Label>
|
||||
<Input
|
||||
value={data.host}
|
||||
onChange={(e) =>
|
||||
@ -115,7 +233,9 @@ function Mail({ data, dispatch, onChange }: CompProps<MailState>) {
|
||||
/>
|
||||
</ParagraphItem>
|
||||
<ParagraphItem>
|
||||
<Label>{t("admin.system.mailPort")}</Label>
|
||||
<Label>
|
||||
<Require /> {t("admin.system.mailPort")}
|
||||
</Label>
|
||||
<NumberInput
|
||||
value={data.port}
|
||||
onValueChange={(value) =>
|
||||
@ -127,7 +247,9 @@ function Mail({ data, dispatch, onChange }: CompProps<MailState>) {
|
||||
/>
|
||||
</ParagraphItem>
|
||||
<ParagraphItem>
|
||||
<Label>{t("admin.system.mailUser")}</Label>
|
||||
<Label>
|
||||
<Require /> {t("admin.system.mailUser")}
|
||||
</Label>
|
||||
<Input
|
||||
value={data.username}
|
||||
onChange={(e) =>
|
||||
@ -140,7 +262,9 @@ function Mail({ data, dispatch, onChange }: CompProps<MailState>) {
|
||||
/>
|
||||
</ParagraphItem>
|
||||
<ParagraphItem>
|
||||
<Label>{t("admin.system.mailPass")}</Label>
|
||||
<Label>
|
||||
<Require /> {t("admin.system.mailPass")}
|
||||
</Label>
|
||||
<Input
|
||||
value={data.password}
|
||||
onChange={(e) =>
|
||||
@ -153,7 +277,9 @@ function Mail({ data, dispatch, onChange }: CompProps<MailState>) {
|
||||
/>
|
||||
</ParagraphItem>
|
||||
<ParagraphItem>
|
||||
<Label>{t("admin.system.mailFrom")}</Label>
|
||||
<Label>
|
||||
<Require /> {t("admin.system.mailFrom")}
|
||||
</Label>
|
||||
<Input
|
||||
value={data.from}
|
||||
onChange={(e) =>
|
||||
@ -200,7 +326,11 @@ function Mail({ data, dispatch, onChange }: CompProps<MailState>) {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button size={`sm`} loading={true} onClick={async () => await onChange()}>
|
||||
<Button
|
||||
size={`sm`}
|
||||
loading={true}
|
||||
onClick={async () => await onChange()}
|
||||
>
|
||||
{t("admin.system.save")}
|
||||
</Button>
|
||||
</ParagraphFooter>
|
||||
@ -245,7 +375,11 @@ function Search({ data, dispatch, onChange }: CompProps<SearchState>) {
|
||||
<ParagraphDescription>{t("admin.system.searchTip")}</ParagraphDescription>
|
||||
<ParagraphFooter>
|
||||
<div className={`grow`} />
|
||||
<Button size={`sm`} loading={true} onClick={async () => await onChange()}>
|
||||
<Button
|
||||
size={`sm`}
|
||||
loading={true}
|
||||
onClick={async () => await onChange()}
|
||||
>
|
||||
{t("admin.system.save")}
|
||||
</Button>
|
||||
</ParagraphFooter>
|
||||
|
@ -220,3 +220,15 @@ export function scrollDown(el: HTMLElement | null) {
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
|
||||
export function updateFavicon(url: string) {
|
||||
/**
|
||||
* Update favicon in the link element from head
|
||||
* @param url Favicon url
|
||||
* @example
|
||||
* updateFavicon("https://example.com/favicon.ico");
|
||||
*/
|
||||
|
||||
const link = document.querySelector("link[rel*='icon']");
|
||||
return link && link.setAttribute("href", url);
|
||||
}
|
||||
|
@ -1,3 +1,13 @@
|
||||
import { updateFavicon } from "@/utils/dom.ts";
|
||||
|
||||
export let appName =
|
||||
localStorage.getItem("app_name") ||
|
||||
import.meta.env.VITE_APP_NAME ||
|
||||
"Chat Nio";
|
||||
export let appLogo =
|
||||
localStorage.getItem("app_logo") ||
|
||||
import.meta.env.VITE_APP_LOGO ||
|
||||
"/favicon.ico";
|
||||
export const useDeeptrain = !!import.meta.env.VITE_USE_DEEPTRAIN;
|
||||
export const backendEndpoint = import.meta.env.VITE_BACKEND_ENDPOINT || "/api";
|
||||
export const blobEndpoint =
|
||||
@ -10,6 +20,9 @@ export const deeptrainAppName = import.meta.env.VITE_DEEPTRAIN_APP || "chatnio";
|
||||
export const deeptrainApiEndpoint =
|
||||
import.meta.env.VITE_DEEPTRAIN_API_ENDPOINT || "https://api.deeptrain.net";
|
||||
|
||||
document.title = appName;
|
||||
updateFavicon(appLogo);
|
||||
|
||||
export function getDev(): boolean {
|
||||
/**
|
||||
* return if the current environment is development
|
||||
@ -47,3 +60,29 @@ export function getTokenField(deploy: boolean): string {
|
||||
*/
|
||||
return deploy ? "token" : "token-dev";
|
||||
}
|
||||
|
||||
export function setAppName(name: string): void {
|
||||
/**
|
||||
* set the app name in localStorage
|
||||
*/
|
||||
name = name.trim();
|
||||
if (name.length === 0) return;
|
||||
|
||||
localStorage.setItem("app_name", name);
|
||||
appName = name;
|
||||
|
||||
document.title = name;
|
||||
}
|
||||
|
||||
export function setAppLogo(logo: string): void {
|
||||
/**
|
||||
* set the app logo in localStorage
|
||||
*/
|
||||
logo = logo.trim();
|
||||
if (logo.length === 0) return;
|
||||
|
||||
localStorage.setItem("app_logo", logo);
|
||||
appLogo = logo;
|
||||
|
||||
updateFavicon(logo);
|
||||
}
|
||||
|
22
auth/auth.go
22
auth/auth.go
@ -4,6 +4,7 @@ import (
|
||||
"chat/channel"
|
||||
"chat/globals"
|
||||
"chat/utils"
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -244,14 +245,31 @@ func Reset(c *gin.Context, form ResetForm) error {
|
||||
return errors.New("invalid email verification code")
|
||||
}
|
||||
|
||||
user := GetUserByEmail(db, email)
|
||||
if user == nil {
|
||||
return errors.New("cannot find user by email")
|
||||
}
|
||||
|
||||
if err := user.UpdatePassword(db, cache, password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cache.Del(c, fmt.Sprintf("nio:otp:%s", email))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) UpdatePassword(db *sql.DB, cache *redis.Client, password string) error {
|
||||
hash := utils.Sha2Encrypt(password)
|
||||
|
||||
if _, err := db.Exec(`
|
||||
UPDATE auth SET password = ? WHERE email = ?
|
||||
`, hash, email); err != nil {
|
||||
UPDATE auth SET password = ? WHERE id = ?
|
||||
`, hash, u.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cache.Del(context.Background(), fmt.Sprintf("nio:user:%s", u.Username))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,22 @@ func GetUserById(db *sql.DB, id int64) *User {
|
||||
return &user
|
||||
}
|
||||
|
||||
func GetUserByName(db *sql.DB, username string) *User {
|
||||
var user User
|
||||
if err := db.QueryRow("SELECT id, username FROM auth WHERE username = ?", username).Scan(&user.ID, &user.Username); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &user
|
||||
}
|
||||
|
||||
func GetUserByEmail(db *sql.DB, email string) *User {
|
||||
var user User
|
||||
if err := db.QueryRow("SELECT id, username FROM auth WHERE email = ?", email).Scan(&user.ID, &user.Username); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &user
|
||||
}
|
||||
|
||||
func GetId(db *sql.DB, user *User) int64 {
|
||||
if user == nil {
|
||||
return -1
|
||||
|
@ -6,6 +6,10 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GetInfo(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, SystemInstance.AsInfo())
|
||||
}
|
||||
|
||||
func DeleteChannel(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
state := ConduitInstance.DeleteChannel(utils.ParseInt(id))
|
||||
|
@ -3,6 +3,8 @@ package channel
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
func Register(app *gin.RouterGroup) {
|
||||
app.GET("/info", GetInfo)
|
||||
|
||||
app.GET("/admin/channel/list", GetChannelList)
|
||||
app.POST("/admin/channel/create", CreateChannel)
|
||||
app.GET("/admin/channel/get/:id", GetChannel)
|
||||
|
@ -5,7 +5,14 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type ApiInfo struct {
|
||||
Title string `json:"title"`
|
||||
Logo string `json:"logo"`
|
||||
}
|
||||
|
||||
type generalState struct {
|
||||
Title string `json:"title" mapstructure:"title"`
|
||||
Logo string `json:"logo" mapstructure:"logo"`
|
||||
Backend string `json:"backend" mapstructure:"backend"`
|
||||
}
|
||||
|
||||
@ -47,6 +54,13 @@ func (c *SystemConfig) SaveConfig() error {
|
||||
return viper.WriteConfig()
|
||||
}
|
||||
|
||||
func (c *SystemConfig) AsInfo() ApiInfo {
|
||||
return ApiInfo{
|
||||
Title: c.General.Title,
|
||||
Logo: c.General.Logo,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SystemConfig) UpdateConfig(data *SystemConfig) error {
|
||||
c.General = data.General
|
||||
c.Mail = data.Mail
|
||||
|
25
cli/admin.go
Normal file
25
cli/admin.go
Normal file
@ -0,0 +1,25 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"chat/admin"
|
||||
"chat/connection"
|
||||
"errors"
|
||||
)
|
||||
|
||||
func UpdateRootCommand(args []string) {
|
||||
db := connection.ConnectMySQL()
|
||||
cache := connection.ConnectRedis()
|
||||
|
||||
if len(args) == 0 {
|
||||
outputError(errors.New("invalid arguments, please provide a new root password"))
|
||||
return
|
||||
}
|
||||
|
||||
password := args[0]
|
||||
if err := admin.UpdateRootPassword(db, cache, password); err != nil {
|
||||
outputError(err)
|
||||
return
|
||||
}
|
||||
|
||||
outputInfo("root", "root password updated")
|
||||
}
|
@ -10,17 +10,17 @@ func Run() bool {
|
||||
switch args[0] {
|
||||
case "help":
|
||||
Help()
|
||||
return true
|
||||
case "invite":
|
||||
CreateInvitationCommand(param)
|
||||
return true
|
||||
case "filter":
|
||||
FilterApiKeyCommand(param)
|
||||
return true
|
||||
case "token":
|
||||
CreateTokenCommand(param)
|
||||
return true
|
||||
case "root":
|
||||
UpdateRootCommand(param)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ Commands:
|
||||
- help
|
||||
- invite <type> <num> <quota>
|
||||
- token <user-id>
|
||||
- root <password>
|
||||
`
|
||||
|
||||
func Help() {
|
||||
|
@ -63,7 +63,7 @@ func GetArgString(args []string, idx int) string {
|
||||
}
|
||||
|
||||
func outputError(err error) {
|
||||
fmt.Println(fmt.Sprintf("[cli] error: %s", err.Error()))
|
||||
fmt.Println(fmt.Sprintf("\033[31m[cli] error: %s\033[0m", err.Error()))
|
||||
}
|
||||
|
||||
func outputInfo(t, msg string) {
|
||||
|
Loading…
Reference in New Issue
Block a user