diff --git a/app/public/robots.txt b/app/public/robots.txt
index f6e6d1d4..4baa86ee 100644
--- a/app/public/robots.txt
+++ b/app/public/robots.txt
@@ -1,2 +1,3 @@
User-Agent: *
Allow: /
+Disallow: /admin/
diff --git a/app/src/components/app/MenuBar.tsx b/app/src/components/app/MenuBar.tsx
index dbf43196..2daa5b5c 100644
--- a/app/src/components/app/MenuBar.tsx
+++ b/app/src/components/app/MenuBar.tsx
@@ -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) {
{t("pkg.title")}
+ dispatch(openInvitationDialog())}>
+
+ {t("invitation.title")}
+
dispatch(openSharingDialog())}>
{t("share.manage")}
diff --git a/app/src/conf.ts b/app/src/conf.ts
index c8521b57..73a04d1d 100644
--- a/app/src/conf.ts
+++ b/app/src/conf.ts
@@ -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";
diff --git a/app/src/conversation/invitation.ts b/app/src/conversation/invitation.ts
new file mode 100644
index 00000000..2c531d11
--- /dev/null
+++ b/app/src/conversation/invitation.ts
@@ -0,0 +1,17 @@
+import axios from "axios";
+
+export type InvitationResponse = {
+ status: boolean;
+ error: string;
+ quota: number;
+}
+
+export async function getInvitation(code: string): Promise {
+ 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 };
+ }
+}
diff --git a/app/src/dialogs/ApiKey.tsx b/app/src/dialogs/ApiKey.tsx
index 71dec299..3a565144 100644
--- a/app/src/dialogs/ApiKey.tsx
+++ b/app/src/dialogs/ApiKey.tsx
@@ -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;
diff --git a/app/src/dialogs/Invitation.tsx b/app/src/dialogs/Invitation.tsx
index e69de29b..8e62a5e2 100644
--- a/app/src/dialogs/Invitation.tsx
+++ b/app/src/dialogs/Invitation.tsx
@@ -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 (
+
+ );
+}
+
+export default Invitation;
diff --git a/app/src/dialogs/index.tsx b/app/src/dialogs/index.tsx
index d8195084..cb89d569 100644
--- a/app/src/dialogs/index.tsx
+++ b/app/src/dialogs/index.tsx
@@ -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() {
+
>
);
}
diff --git a/app/src/store/index.ts b/app/src/store/index.ts
index d558532f..1fae2fbb 100644
--- a/app/src/store/index.ts
+++ b/app/src/store/index.ts
@@ -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,
},
});
diff --git a/app/src/store/invitation.ts b/app/src/store/invitation.ts
new file mode 100644
index 00000000..117b063f
--- /dev/null
+++ b/app/src/store/invitation.ts
@@ -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;
diff --git a/auth/invitation.go b/auth/invitation.go
index 4c277c8a..e81db5ed 100644
--- a/auth/invitation.go
+++ b/auth/invitation.go
@@ -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)
}
diff --git a/cli/exec.go b/cli/exec.go
new file mode 100644
index 00000000..6a33a7af
--- /dev/null
+++ b/cli/exec.go
@@ -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
+ }
+}
diff --git a/cli/help.go b/cli/help.go
new file mode 100644
index 00000000..378f92c9
--- /dev/null
+++ b/cli/help.go
@@ -0,0 +1,13 @@
+package cli
+
+import "fmt"
+
+var Prompt = `
+Commands:
+ - help
+ - invite
+`
+
+func Help() {
+ fmt.Println(fmt.Sprintf("%s", Prompt))
+}
diff --git a/cli/invite.go b/cli/invite.go
new file mode 100644
index 00000000..a09b791e
--- /dev/null
+++ b/cli/invite.go
@@ -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"))
+}
diff --git a/cli/parser.go b/cli/parser.go
new file mode 100644
index 00000000..3258f2fe
--- /dev/null
+++ b/cli/parser.go
@@ -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)
+}
diff --git a/main.go b/main.go
index 8aa75050..caaca276 100644
--- a/main.go
+++ b/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)