update invitation feature

This commit is contained in:
Zhang Minghan 2023-10-25 12:53:16 +08:00
parent c248c3519f
commit 9c9d1cc65d
15 changed files with 262 additions and 8 deletions

View File

@ -1,2 +1,3 @@
User-Agent: *
Allow: /
Disallow: /admin/

View File

@ -18,12 +18,13 @@ import {
BadgeCent,
Boxes,
CalendarPlus,
Cloud,
Cloud, Gift,
ListStart,
Plug,
} from "lucide-react";
import { openDialog as openSub } from "../../store/subscription.ts";
import { openDialog as openPackageDialog } from "../../store/package.ts";
import { openDialog as openInvitationDialog } from "../../store/invitation.ts";
import { openDialog as openSharingDialog } from "../../store/sharing.ts";
import { openDialog as openApiDialog } from "../../store/api.ts";
@ -60,6 +61,10 @@ function MenuBar({ children, className }: MenuBarProps) {
<Boxes className={`h-4 w-4 mr-1`} />
{t("pkg.title")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => dispatch(openInvitationDialog())}>
<Gift className={`h-4 w-4 mr-1`} />
{t("invitation.title")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => dispatch(openSharingDialog())}>
<ListStart className={`h-4 w-4 mr-1`} />
{t("share.manage")}

View File

@ -1,7 +1,7 @@
import axios from "axios";
import { Model } from "./conversation/types.ts";
export const version = "3.5.9";
export const version = "3.5.10";
export const dev: boolean = window.location.hostname === "localhost";
export const deploy: boolean = true;
export let rest_api: string = "http://localhost:8094";

View File

@ -0,0 +1,17 @@
import axios from "axios";
export type InvitationResponse = {
status: boolean;
error: string;
quota: number;
}
export async function getInvitation(code: string): Promise<InvitationResponse> {
try {
const resp = await axios.get(`/invite?code=${code}`);
return resp.data as InvitationResponse;
} catch (e) {
console.debug(e);
return { status: false, error: "network error", quota: 0 };
}
}

View File

@ -23,7 +23,7 @@ import { useToast } from "../components/ui/use-toast.ts";
import { copyClipboard, useEffectAsync } from "../utils.ts";
import { selectInit } from "../store/auth.ts";
function Package() {
function ApiKey() {
const { t } = useTranslation();
const dispatch = useDispatch();
const open = useSelector(dialogSelector);
@ -75,4 +75,4 @@ function Package() {
);
}
export default Package;
export default ApiKey;

View File

@ -0,0 +1,69 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "../components/ui/dialog.tsx";
import { Button } from "../components/ui/button.tsx";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import {
closeDialog,
dialogSelector,
setDialog,
} from "../store/invitation.ts";
import { Input } from "../components/ui/input.tsx";
import { useToast } from "../components/ui/use-toast.ts";
import {useState} from "react";
import {getInvitation} from "../conversation/invitation.ts";
function Invitation() {
const { t } = useTranslation();
const dispatch = useDispatch();
const open = useSelector(dialogSelector);
const { toast } = useToast();
const [code, setCode] = useState("");
return (
<Dialog open={open} onOpenChange={(open) => dispatch(setDialog(open))}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("invitation.title")}</DialogTitle>
<DialogDescription>
<Input
value={code}
placeholder={t("invitation.input-placeholder")}
className={`w-full mt-6 text-center`}
onChange={(e) => setCode(e.target.value)}
/>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant={`outline`} onClick={() => dispatch(closeDialog())}>
{t("invitation.cancel")}
</Button>
<Button onClick={async () => {
const resp = await getInvitation(code.trim());
if (resp.status) {
toast({
title: t("invitation.check-success"),
description: t("invitation.check-success-description", { amount: resp.quota }),
})
dispatch(closeDialog());
}
else toast({
title: t("invitation.check-failed"),
description: resp.error,
})
}}>
{t("invitation.check")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export default Invitation;

View File

@ -4,6 +4,7 @@ import ApiKey from "./ApiKey.tsx";
import Package from "./Package.tsx";
import Subscription from "./Subscription.tsx";
import ShareManagement from "./ShareManagement.tsx";
import Invitation from "./Invitation.tsx";
function DialogManager() {
return (
@ -14,6 +15,7 @@ function DialogManager() {
<Package />
<Subscription />
<ShareManagement />
<Invitation />
</>
);
}

View File

@ -7,6 +7,7 @@ import packageReducer from "./package";
import subscriptionReducer from "./subscription";
import apiReducer from "./api";
import sharingReducer from "./sharing";
import invitationReducer from "./invitation";
const store = configureStore({
reducer: {
@ -18,6 +19,7 @@ const store = configureStore({
subscription: subscriptionReducer,
api: apiReducer,
sharing: sharingReducer,
invitation: invitationReducer,
},
});

View File

@ -0,0 +1,28 @@
import {createSlice} from "@reduxjs/toolkit";
import {RootState} from "./index.ts";
export const invitationSlice = createSlice({
name: "invitation",
initialState: {
dialog: false,
},
reducers: {
toggleDialog: (state) => {
state.dialog = !state.dialog;
},
setDialog: (state, action) => {
state.dialog = action.payload as boolean;
},
openDialog: (state) => {
state.dialog = true;
},
closeDialog: (state) => {
state.dialog = false;
},
}
});
export const {toggleDialog, setDialog, openDialog, closeDialog} = invitationSlice.actions;
export default invitationSlice.reducer;
export const dialogSelector = (state: RootState): boolean => state.invitation.dialog;

View File

@ -16,12 +16,12 @@ type Invitation struct {
UsedId int64 `json:"used_id"`
}
func GenerateCodes(db *sql.DB, num int, quota float32, t string) ([]string, error) {
func GenerateInvitations(db *sql.DB, num int, quota float32, t string) ([]string, error) {
arr := make([]string, 0)
idx := 0
for idx < num {
code := fmt.Sprintf("%s-%s", t, utils.GenerateChar(24))
if err := GenerateCode(db, code, quota, t); err != nil {
if err := CreateInvitationCode(db, code, quota, t); err != nil {
// unique constraint
if errors.Is(err, sql.ErrNoRows) {
continue
@ -35,7 +35,7 @@ func GenerateCodes(db *sql.DB, num int, quota float32, t string) ([]string, erro
return arr, nil
}
func GenerateCode(db *sql.DB, code string, quota float32, t string) error {
func CreateInvitationCode(db *sql.DB, code string, quota float32, t string) error {
_, err := db.Exec(`
INSERT INTO invitation (code, quota, type)
VALUES (?, ?, ?)
@ -50,7 +50,11 @@ func GetInvitation(db *sql.DB, code string) (*Invitation, error) {
WHERE code = ?
`, code)
var invitation Invitation
err := row.Scan(&invitation.Id, &invitation.Code, &invitation.Quota, &invitation.Type, &invitation.Used, &invitation.UsedId)
var id sql.NullInt64
err := row.Scan(&invitation.Id, &invitation.Code, &invitation.Quota, &invitation.Type, &invitation.Used, &id)
if id.Valid {
invitation.UsedId = id.Int64
}
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("invitation code not found")
@ -83,6 +87,8 @@ func (i *Invitation) UseInvitation(db *sql.DB, user User) error {
if err := i.Use(db, user.GetID(db)); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("invitation code not found")
} else if errors.Is(err, sql.ErrTxDone) {
return fmt.Errorf("transaction has been closed")
}
return fmt.Errorf("failed to use invitation: %w", err)
}

19
cli/exec.go Normal file
View File

@ -0,0 +1,19 @@
package cli
func Run() bool {
args := GetArgs()
if len(args) == 0 {
return false
}
switch args[0] {
case "help":
Help()
return true
case "invite":
CreateInvitationCommand(args[1:])
return true
default:
return false
}
}

13
cli/help.go Normal file
View File

@ -0,0 +1,13 @@
package cli
import "fmt"
var Prompt = `
Commands:
- help
- invite <type> <num> <quota>
`
func Help() {
fmt.Println(fmt.Sprintf("%s", Prompt))
}

25
cli/invite.go Normal file
View File

@ -0,0 +1,25 @@
package cli
import (
"chat/auth"
"chat/connection"
"fmt"
"strings"
)
func CreateInvitationCommand(args []string) {
db := connection.ConnectMySQL()
var (
t = GetArgString(args, 0)
num = GetArgInt(args, 1)
quota = GetArgFloat32(args, 2)
)
resp, err := auth.GenerateInvitations(db, num, quota, t)
if err != nil {
panic(err)
}
fmt.Println(strings.Join(resp, "\n"))
}

63
cli/parser.go Normal file
View File

@ -0,0 +1,63 @@
package cli
import (
"fmt"
"log"
"os"
"strconv"
)
func GetArgs() []string {
return os.Args[1:]
}
func GetArg(args []string, idx int) string {
if len(args) <= idx {
log.Fatalln(fmt.Sprintf("not enough arguments: %d", idx))
}
return args[idx]
}
func GetArgInt(args []string, idx int) int {
i, err := strconv.Atoi(GetArg(args, idx))
if err != nil {
log.Fatalln(fmt.Sprintf("invalid argument: %s", err.Error()))
}
return i
}
func GetArgFloat(args []string, idx int, bitSize int) float64 {
f, err := strconv.ParseFloat(GetArg(args, idx), bitSize)
if err != nil {
log.Fatalln(fmt.Sprintf("invalid argument: %s", err.Error()))
}
return f
}
func GetArgFloat32(args []string, idx int) float32 {
return float32(GetArgFloat(args, idx, 32))
}
func GetArgFloat64(args []string, idx int) float64 {
return GetArgFloat(args, idx, 64)
}
func GetArgBool(args []string, idx int) bool {
b, err := strconv.ParseBool(GetArg(args, idx))
if err != nil {
log.Fatalln(fmt.Sprintf("invalid argument: %s", err.Error()))
}
return b
}
func GetArgInt64(args []string, idx int) int64 {
i, err := strconv.ParseInt(GetArg(args, idx), 10, 64)
if err != nil {
log.Fatalln(fmt.Sprintf("invalid argument: %s", err.Error()))
}
return i
}
func GetArgString(args []string, idx int) string {
return GetArg(args, idx)
}

View File

@ -3,6 +3,7 @@ package main
import (
"chat/addition"
"chat/auth"
"chat/cli"
"chat/manager"
"chat/manager/conversation"
"chat/middleware"
@ -17,6 +18,9 @@ func main() {
if err := viper.ReadInConfig(); err != nil {
panic(err)
}
if cli.Run() {
return
}
app := gin.Default()
middleware.RegisterMiddleware(app)