Skip to content

Commit

Permalink
feat: add re-generate apikey and charge unused models alert
Browse files Browse the repository at this point in the history
  • Loading branch information
zmh-program committed Jan 11, 2024
1 parent 63d1cef commit 967eef7
Show file tree
Hide file tree
Showing 16 changed files with 203 additions and 20 deletions.
2 changes: 1 addition & 1 deletion app/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"package": {
"productName": "chatnio",
"version": "3.8.4"
"version": "3.8.5"
},
"tauri": {
"allowlist": {
Expand Down
3 changes: 2 additions & 1 deletion app/src/admin/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export type CommonResponse = {
status: boolean;
error: string;
reason?: string;
};

export function toastState(
Expand All @@ -12,5 +13,5 @@ export function toastState(
if (state.status)
toastSuccess &&
toast({ title: t("success"), description: t("request-success") });
else toast({ title: t("error"), description: state.error });
else toast({ title: t("error"), description: state.error ?? state.reason });
}
17 changes: 17 additions & 0 deletions app/src/api/addition.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axios from "axios";
import { getErrorMessage } from "@/utils/base.ts";

type QuotaResponse = {
status: boolean;
Expand Down Expand Up @@ -30,6 +31,12 @@ type ApiKeyResponse = {
key: string;
};

type ResetApiKeyResponse = {
status: boolean;
key: string;
error: string;
};

export async function buyQuota(quota: number): Promise<QuotaResponse> {
try {
const resp = await axios.post(`/buy`, { quota });
Expand Down Expand Up @@ -110,3 +117,13 @@ export async function getKey(): Promise<ApiKeyResponse> {
return { status: false, key: "" };
}
}

export async function regenerateKey(): Promise<ResetApiKeyResponse> {
try {
const resp = await axios.post(`/resetkey`);
return resp.data as ResetApiKeyResponse;
} catch (e) {
console.debug(e);
return { status: false, key: "", error: getErrorMessage(e) };
}
}
16 changes: 16 additions & 0 deletions app/src/assets/admin/charge.less
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@
}
}

.charge-alert {
.model-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;

.model {
padding: 0.5rem 0.75rem;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
}
}
}

.charge-editor {
padding: 1.5rem;
border: 1px solid hsl(var(--border));
Expand Down
38 changes: 38 additions & 0 deletions app/src/components/admin/ChargeWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Input } from "@/components/ui/input.tsx";
import { useMemo, useReducer, useState } from "react";
import { Button } from "@/components/ui/button.tsx";
import {
AlertCircle,
Cloud,
DownloadCloud,
Eraser,
Expand Down Expand Up @@ -54,6 +55,9 @@ import { useToast } from "@/components/ui/use-toast";
import { deleteCharge, listCharge, setCharge } from "@/admin/api/charge.ts";
import { useEffectAsync } from "@/utils/hook.ts";
import { cn } from "@/components/ui/lib/utils.ts";
import { allModels } from "@/conf.ts";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
import Tips from "@/components/Tips.tsx";

const initialState: ChargeProps = {
id: -1,
Expand Down Expand Up @@ -120,6 +124,33 @@ function preflight(state: ChargeProps): ChargeProps {
return state;
}

type ChargeAlertProps = {
models: string[];
};

function ChargeAlert({ models }: ChargeAlertProps) {
const { t } = useTranslation();

return (
models.length > 0 && (
<Alert className={`charge-alert`}>
<AlertTitle className={`flex flex-row items-center select-none`}>
<AlertCircle className="h-4 w-4 mr-2" />
<p>{t("admin.charge.unused-model")}</p>
<Tips content={t("admin.charge.unused-model-tip")} />
</AlertTitle>
<AlertDescription className={`model-list`}>
{models.map((model, index) => (
<div key={index} className={`model`}>
{model}
</div>
))}
</AlertDescription>
</Alert>
)
);
}

type ChargeEditorProps = {
form: ChargeProps;
dispatch: (action: any) => void;
Expand Down Expand Up @@ -469,6 +500,12 @@ function ChargeWidget() {
return data.flatMap((charge) => charge.models);
}, [data]);

const unusedModels = useMemo(() => {
return allModels.filter(
(model) => !usedModels.includes(model) && model.trim() !== "",
);
}, [allModels, usedModels]);

async function refresh() {
setLoading(true);
const resp = await listCharge();
Expand All @@ -481,6 +518,7 @@ function ChargeWidget() {

return (
<div className={`charge-widget`}>
<ChargeAlert models={unusedModels} />
<ChargeEditor
onRefresh={refresh}
form={form}
Expand Down
2 changes: 1 addition & 1 deletion app/src/conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import React from "react";
import { syncSiteInfo } from "@/admin/api/info.ts";
import { getOfflineModels, loadPreferenceModels } from "@/utils/storage.ts";

export const version = "3.8.4";
export const version = "3.8.5";
export const dev: boolean = getDev();
export const deploy: boolean = true;
export let rest_api: string = getRestApi(deploy);
Expand Down
69 changes: 61 additions & 8 deletions app/src/dialogs/ApikeyDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,27 @@ import {
setDialog,
keySelector,
getApiKey,
regenerateApiKey,
} from "@/store/api.ts";
import { Input } from "@/components/ui/input.tsx";
import { Copy, ExternalLink, RotateCw } from "lucide-react";
import { Copy, ExternalLink, Power, RotateCw } from "lucide-react";
import { useToast } from "@/components/ui/use-toast.ts";
import { copyClipboard } from "@/utils/dom.ts";
import { useEffectAsync } from "@/utils/hook.ts";
import { selectInit } from "@/store/auth.ts";
import { docsEndpoint } from "@/utils/env.ts";
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog.tsx";
import { useState } from "react";
import { CommonResponse, toastState } from "@/admin/utils.ts";

function ApikeyDialog() {
const { t } = useTranslation();
Expand All @@ -33,6 +46,8 @@ function ApikeyDialog() {
const { toast } = useToast();
const init = useSelector(selectInit);

const [openReset, setOpenReset] = useState(false);

useEffectAsync(async () => {
if (init) await getApiKey(dispatch);
}, [init]);
Expand All @@ -45,6 +60,15 @@ function ApikeyDialog() {
});
}

async function resetKey() {
const resp = await regenerateApiKey(dispatch);
toastState(toast, t, resp as CommonResponse, true);

if (resp.status) {
setOpenReset(false);
}
}

return (
<Dialog open={open} onOpenChange={(open) => dispatch(setDialog(open))}>
<DialogContent>
Expand All @@ -60,17 +84,46 @@ function ApikeyDialog() {
>
<RotateCw className={`h-4 w-4`} />
</Button>
<Input value={key} />
<Input value={key} readOnly={true} />
<Button variant={`default`} size={`icon`} onClick={copyKey}>
<Copy className={`h-4 w-4`} />
</Button>
</div>
<Button variant={`outline`} asChild>
<a href={docsEndpoint} target={`_blank`}>
<ExternalLink className={`h-4 w-4 mr-2`} />
{t("buy.learn-more")}
</a>
</Button>
<div className={`flex flex-row`}>
<AlertDialog open={openReset} onOpenChange={setOpenReset}>
<AlertDialogTrigger asChild>
<Button variant={`destructive`} className={`mr-2`}>
<Power className={`h-4 w-4 mr-2`} />
{t("api.reset")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("api.reset")}</AlertDialogTitle>
<AlertDialogDescription>
{t("api.reset-description")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<Button
variant={`destructive`}
loading={true}
onClick={resetKey}
>
{t("confirm")}
</Button>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

<Button variant={`outline`} asChild>
<a href={docsEndpoint} target={`_blank`}>
<ExternalLink className={`h-4 w-4 mr-2`} />
{t("api.learn-more")}
</a>
</Button>
</div>
</div>
</DialogDescription>
</DialogHeader>
Expand Down
8 changes: 6 additions & 2 deletions app/src/resources/i18n/cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@
"title": "API 设置",
"copied": "复制成功",
"copied-description": "API 密钥已复制到剪贴板",
"learn-more": "了解更多"
"learn-more": "了解更多",
"reset": "重置密钥",
"reset-description": "是否确定?此操作无法撤消。这将永久重置 API 密钥,已有 API 密钥将会失效。"
},
"service": {
"title": "发现新版本",
Expand Down Expand Up @@ -481,7 +483,9 @@
"input-count": "输入点数",
"output-count": "输出点数",
"add-rule": "添加规则",
"update-rule": "更新规则"
"update-rule": "更新规则",
"unused-model": "部分模型计费规则未设置",
"unused-model-tip": "计费规则未设置的模型将不会计费并支持匿名调用,请谨慎设置。"
},
"system": {
"general": "常规设置",
Expand Down
8 changes: 6 additions & 2 deletions app/src/resources/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,9 @@
"title": "API Settings",
"copied": "Copied",
"copied-description": "API key has been copied to clipboard",
"learn-more": "Learn more"
"learn-more": "Learn more",
"reset": "Reset Secret Key",
"reset-description": "Are you sure? This action cannot be undone. This will permanently reset the API key and the existing API key will expire."
},
"service": {
"title": "New Version Available",
Expand Down Expand Up @@ -408,7 +410,9 @@
"input-count": "Input Quota",
"output-count": "Output Quota",
"add-rule": "Add Rule",
"update-rule": "Update Rule"
"update-rule": "Update Rule",
"unused-model": "Some model billing rules are not set",
"unused-model-tip": "Models that do not have billing rules set will not be billed and will support anonymous calls, please set them carefully."
},
"system": {
"general": "General Settings",
Expand Down
8 changes: 6 additions & 2 deletions app/src/resources/i18n/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,9 @@
"title": "API設定",
"copied": "コピー成功",
"copied-description": "APIキーをクリップボードにコピーしました",
"learn-more": "詳細はこちら"
"learn-more": "詳細はこちら",
"reset": "キーをリセット",
"reset-description": "本当によろしいですか?この操作は元に戻せません。これにより、APIキーが完全にリセットされ、既存のAPIキーが期限切れになります。"
},
"service": {
"title": "新しいバージョンを発見する",
Expand Down Expand Up @@ -408,7 +410,9 @@
"input-count": "ポイントを入力",
"output-count": "出力ポイント",
"add-rule": "規則の追加",
"update-rule": "ルールを更新"
"update-rule": "ルールを更新",
"unused-model": "一部のモデルの請求ルールが設定されていません",
"unused-model-tip": "請求ルールが設定されていないモデルは請求されず、匿名通話をサポートします。慎重に設定してください。"
},
"system": {
"general": "全般設定",
Expand Down
8 changes: 6 additions & 2 deletions app/src/resources/i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,9 @@
"title": "Настройки API",
"copied": "Скопировано",
"copied-description": "Ключ API скопирован в буфер обмена",
"learn-more": "Узнать больше"
"learn-more": "Узнать больше",
"reset": "Кнопка сброса",
"reset-description": "Вы уверены? Это действие нельзя отменить. Это приведет к окончательному сбросу ключа API, и срок действия существующего ключа API истечет."
},
"service": {
"title": "Доступна новая версия",
Expand Down Expand Up @@ -408,7 +410,9 @@
"input-count": "Квота входа",
"output-count": "Квота выхода",
"add-rule": "Добавить правило",
"update-rule": "Обновить правило"
"update-rule": "Обновить правило",
"unused-model": "Некоторые правила выставления счетов модели не установлены",
"unused-model-tip": "Модели, которые не имеют установленных правил выставления счетов, не будут выставляться счета и будут поддерживать анонимные звонки, пожалуйста, установите их тщательно."
},
"system": {
"general": "Общие настройки",
Expand Down
11 changes: 10 additions & 1 deletion app/src/store/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createSlice } from "@reduxjs/toolkit";
import { getKey } from "@/api/addition.ts";
import { getKey, regenerateKey } from "@/api/addition.ts";
import { AppDispatch, RootState } from "./index.ts";

export const apiSlice = createSlice({
Expand Down Expand Up @@ -44,3 +44,12 @@ export const getApiKey = async (dispatch: AppDispatch, retries?: boolean) => {
dispatch(setKey(response.key));
}
};

export const regenerateApiKey = async (dispatch: AppDispatch) => {
const response = await regenerateKey();
if (response.status) {
dispatch(setKey(response.key));
}

return response;
};
8 changes: 8 additions & 0 deletions auth/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package auth
import (
"chat/utils"
"database/sql"
"errors"
"fmt"
)

Expand All @@ -22,3 +23,10 @@ func (u *User) GetApiKey(db *sql.DB) string {
}
return key
}

func (u *User) ResetApiKey(db *sql.DB) (string, error) {
if _, err := db.Exec("DELETE FROM apikey WHERE user_id = ?", u.GetID(db)); err != nil && !errors.Is(err, sql.ErrNoRows) {
return "", err
}
return u.CreateApiKey(db), nil
}
Loading

0 comments on commit 967eef7

Please sign in to comment.