Skip to content

Commit

Permalink
update invitation feature
Browse files Browse the repository at this point in the history
zmh-program committed Oct 25, 2023
1 parent c248c35 commit 9c9d1cc
Showing 15 changed files with 262 additions and 8 deletions.
1 change: 1 addition & 0 deletions app/public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
User-Agent: *
Allow: /
Disallow: /admin/
7 changes: 6 additions & 1 deletion app/src/components/app/MenuBar.tsx
Original file line number Diff line number Diff line change
@@ -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")}
2 changes: 1 addition & 1 deletion app/src/conf.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 17 additions & 0 deletions app/src/conversation/invitation.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
}
4 changes: 2 additions & 2 deletions app/src/dialogs/ApiKey.tsx
Original file line number Diff line number Diff line change
@@ -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;
69 changes: 69 additions & 0 deletions app/src/dialogs/Invitation.tsx
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions app/src/dialogs/index.tsx
Original file line number Diff line number Diff line change
@@ -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 />
</>
);
}
2 changes: 2 additions & 0 deletions app/src/store/index.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 28 additions & 0 deletions app/src/store/invitation.ts
Original file line number Diff line number Diff line change
@@ -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;
14 changes: 10 additions & 4 deletions auth/invitation.go
Original file line number Diff line number Diff line change
@@ -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 changes: 19 additions & 0 deletions cli/exec.go
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions cli/help.go
Original file line number Diff line number Diff line change
@@ -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 changes: 25 additions & 0 deletions cli/invite.go
Original file line number Diff line number Diff line change
@@ -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 changes: 63 additions & 0 deletions cli/parser.go
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 9c9d1cc

Please sign in to comment.