feat: add custom website title and logo feature; support update root password in admin system settings

This commit is contained in:
Zhang Minghan 2024-01-03 13:14:54 +08:00
parent 89e5e13f97
commit 4efd0e8928
31 changed files with 443 additions and 30 deletions

View File

@ -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,
})
}

View File

@ -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)
}

View File

@ -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
View 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);
});
}

View File

@ -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: {

View File

@ -15,6 +15,7 @@
.logo {
width: 4rem;
height: 4rem;
border-radius: var(--radius);
}
& > * {

View File

@ -31,6 +31,7 @@
margin: 2px;
width: 40px;
height: 40px;
border-radius: var(--radius);
cursor: pointer;
}

View File

@ -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;

View File

@ -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>;
}

View File

@ -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("/")}
/>

View File

@ -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();

View File

@ -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": {

View File

@ -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",

View File

@ -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": "招待コード",

View File

@ -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": "Код приглашения",

View File

@ -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`}>

View File

@ -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`}>

View File

@ -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>
)}

View File

@ -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 ? (

View File

@ -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>

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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
}

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -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
View 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")
}

View File

@ -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
}

View File

@ -7,6 +7,7 @@ Commands:
- help
- invite <type> <num> <quota>
- token <user-id>
- root <password>
`
func Help() {

View File

@ -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) {