mirror of
https://github.com/coaidev/coai.git
synced 2025-05-20 05:20:15 +09:00
update invitation feature
This commit is contained in:
parent
c248c3519f
commit
9c9d1cc65d
@ -1,2 +1,3 @@
|
||||
User-Agent: *
|
||||
Allow: /
|
||||
Disallow: /admin/
|
||||
|
@ -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")}
|
||||
|
@ -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";
|
||||
|
17
app/src/conversation/invitation.ts
Normal file
17
app/src/conversation/invitation.ts
Normal 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 };
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
28
app/src/store/invitation.ts
Normal file
28
app/src/store/invitation.ts
Normal 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;
|
@ -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
19
cli/exec.go
Normal 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
13
cli/help.go
Normal 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
25
cli/invite.go
Normal 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
63
cli/parser.go
Normal 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)
|
||||
}
|
4
main.go
4
main.go
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user