init repo

This commit is contained in:
VaalaCat 2024-08-28 00:02:28 +08:00 committed by vaalacat
commit 13148b95e3
97 changed files with 10214 additions and 0 deletions

39
.dockerignore Normal file
View File

@ -0,0 +1,39 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
config.yaml
.drone.yml

146
.drone.yml Normal file
View File

@ -0,0 +1,146 @@
kind: pipeline
name: build-and-publish
steps:
- name: download modules
image: git.vaala.cloud/vaalacat/golang:1.21-alpine
commands:
- sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
- apk update --no-cache && apk add --no-cache tzdata git
- CGO_ENABLED=0 GOPRIVATE=git.vaala.cloud GOPROXY=https://goproxy.cn,https://proxy.golang.org,direct go mod download
- mkdir -p etc
- cp /etc/ssl/certs/ca-certificates.crt ./etc/ca-certificates.crt
- cp /usr/share/zoneinfo/Asia/Shanghai ./etc/Shanghai
volumes:
- name: gocache
path: /go/pkg/mod
- name: build
path: /tmp/app
when:
event:
- pull_request
- promote
- rollback
- name: build frontend
image: git.vaala.cloud/vaalacat/node:20-alpine
commands:
- sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
- apk update --no-cache && apk add --no-cache tzdata git openssh curl
- mkdir -p ~/.ssh
- npm install -g pnpm
- ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts
- corepack enable
- corepack prepare pnpm@latest-7 --activate
- pnpm config set store-dir /root/.pnpm-store
- pnpm install --no-frozen-lockfile
- pnpm build
# - curl -sSf https://sshx.io/get | sh
# - sshx
# - yarn install
# - yarn run build
volumes:
- name: nodecache
path: /root/.pnpm-store
when:
event:
- pull_request
- promote
- rollback
- name: build - amd64
image: git.vaala.cloud/vaalacat/golang:1.21-alpine
commands:
- CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPRIVATE=git.vaala.cloud GOPROXY=https://goproxy.cn,https://proxy.golang.org,direct go build -ldflags="-s -w" -o toyboom-server-amd64 cmd/*.go
volumes:
- name: gocache
path: /go/pkg/mod
- name: build
path: /tmp/app
depends_on:
- build frontend
- download modules
when:
event:
- pull_request
- promote
- rollback
- name: build - arm64
image: git.vaala.cloud/vaalacat/golang:1.21-alpine
commands:
- CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GOPRIVATE=git.vaala.cloud GOPROXY=https://goproxy.cn,https://proxy.golang.org,direct go build -ldflags="-s -w" -o toyboom-server-arm64 cmd/*.go
volumes:
- name: gocache
path: /go/pkg/mod
- name: build
path: /tmp/app
depends_on:
- build frontend
- download modules
when:
event:
- pull_request
- promote
- rollback
- name: publish - amd64
image: git.vaala.cloud/vaalacat/drone-docker-buildx:24
privileged: true
settings:
debug: false
platforms:
- linux/amd64
build_args:
- ARCH=amd64
repo: git.vaala.cloud/sharkai/toyboom-server
tags:
- amd64-c3e691e3
registry:
from_secret: DOCKER_REGISTRY
username:
from_secret: DOCKER_USERNAME
password:
from_secret: DOCKER_PASSWORD
depends_on:
- build - amd64
when:
event:
- promote
- rollback
target:
- production
- name: publish - arm64
image: git.vaala.cloud/vaalacat/drone-docker-buildx:24
privileged: true
settings:
debug: false
platforms:
- linux/arm64
build_args:
- ARCH=arm64
repo: git.vaala.cloud/sharkai/toyboom-server
tags:
- arm64-c3e691e3
registry:
from_secret: DOCKER_REGISTRY
username:
from_secret: DOCKER_USERNAME
password:
from_secret: DOCKER_PASSWORD
depends_on:
- build - arm64
when:
event:
- promote
- rollback
target:
- production
volumes:
- name: build
temp: {}
- name: gocache
host:
path: /tmp/drone/github.com/nose7en/ToyBoomServer/gocache
- name: nodecache
host:
path: /tmp/drone/github.com/nose7en/ToyBoomServer/nodecache

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

47
.gitignore vendored Normal file
View File

@ -0,0 +1,47 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
/.vscode
# testing
/coverage
/test
# next.js
/.next/
/out/
# production
build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
config.yaml
cmd/out
tmp
*.http
sadsad
cmd/*.exe
test
*.db

16
Dockerfile Normal file
View File

@ -0,0 +1,16 @@
FROM alpine
ARG ARCH
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
apk update --no-cache && apk --no-cache add curl bash fuse3 sqlite
ENV TZ Asia/Shanghai
WORKDIR /app
COPY ./toyboom-server-${ARCH} /app/toyboom-server
COPY ./etc /app/etc
RUN ln -sf /app/etc/Shanghai /etc/localtime && mv /app/etc/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
CMD [ "/app/toyboom-server" ]

12
Dockerfile.standalone Normal file
View File

@ -0,0 +1,12 @@
FROM golang:1.22 AS builder
WORKDIR $GOPATH/src/toyboom-server
COPY . .
RUN mkdir /app && \
CGO_ENABLED=1 GOPROXY=https://goproxy.cn,direct go build -o toyboom-server main.go && \
cp toyboom-server /app/
FROM golang:1.22
COPY --from=builder /app/toyboom-server /app/toyboom-server
EXPOSE 8080
ENTRYPOINT [ "/app/toyboom-server" ]

62
README.md Normal file
View File

@ -0,0 +1,62 @@
# Toyboom Server
```
.
├── Dockerfile
├── Dockerfile.standalone
├── README.md
├── biz
│ ├── handler.go
│ ├── static.go
│ └── user
│ ├── create.go
│ └── get.go
├── cmd
│ ├── db.go
│ └── main.go
├── common
│ ├── const.go
│ ├── context.go
│ ├── helper.go
│ ├── logger.go
│ ├── logger_remote.go
│ ├── request.go
│ ├── response.go
│ └── result.go
├── config
│ ├── helper.go
│ └── settings.go
├── dao
│ ├── interface.go
│ └── user.go
├── defs
│ ├── entity.go
│ ├── request.go
│ ├── response.go
│ └── user_info.go
├── go.mod
├── go.sum
├── middleware
│ ├── auth.go
│ └── init.go
├── models
│ ├── models.go
│ └── user.go
├── next.config.mjs
├── rpc
│ ├── apple.go
│ └── manager.go
├── services
│ └── api
│ └── handler.go
├── storage
│ └── db.go
├── toyboom.db
├── utils
│ ├── base64.go
│ ├── crypto.go
│ ├── idgen.go
│ └── jwt.go
└── watcher
└── corn_service.go
```

29
biz/handler.go Normal file
View File

@ -0,0 +1,29 @@
package biz
import (
"github.com/nose7en/ToyBoomServer/biz/user"
"github.com/nose7en/ToyBoomServer/common"
"github.com/nose7en/ToyBoomServer/config"
"github.com/nose7en/ToyBoomServer/middleware"
"github.com/gin-gonic/gin"
)
func Router() *gin.Engine {
r := gin.Default()
api := r.Group("/api")
v1 := api.Group("/v1")
userRouter := v1.Group("/user")
{
userRouter.POST("/create", middleware.ValidateAppleAppToken(), common.Wrapper(user.CreateUser))
userRouter.GET("/info", middleware.ValidateAppleAppToken(), common.Wrapper(user.GetUserInfo))
}
if config.IsDebug() {
// for debug
v1.GET("/ping", middleware.ValidateAppleAppToken(), func(ctx *gin.Context) { ctx.JSON(200, gin.H{"message": "pong"}) })
}
return r
}

53
biz/static.go Normal file
View File

@ -0,0 +1,53 @@
package biz
import (
"embed"
"io/fs"
"net/http"
"strings"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
)
type embedFileSystem struct {
http.FileSystem
}
func (e embedFileSystem) Exists(prefix string, path string) bool {
_, err := e.Open(path)
return err == nil
}
func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
fsys, err := fs.Sub(fsEmbed, targetPath)
if err != nil {
panic(err)
}
return embedFileSystem{
FileSystem: http.FS(fsys),
}
}
func HandleStaticFile(router *gin.Engine, f embed.FS) {
root := EmbedFolder(f, "out")
router.Use(static.Serve("/", root))
staticServer := static.Serve("/", root)
router.NoRoute(func(c *gin.Context) {
if c.Request.Method == http.MethodGet &&
!strings.ContainsRune(c.Request.URL.Path, '.') &&
!strings.HasPrefix(c.Request.URL.Path, "/v1/") {
if strings.HasSuffix(c.Request.URL.Path, "/") {
c.Request.URL.Path += "index.html"
staticServer(c)
return
}
if !strings.HasSuffix(c.Request.URL.Path, ".html") {
c.Request.URL.Path += ".html"
staticServer(c)
return
}
}
})
}

24
biz/user/create.go Normal file
View File

@ -0,0 +1,24 @@
package user
import (
"context"
"github.com/nose7en/ToyBoomServer/common"
"github.com/nose7en/ToyBoomServer/dao"
"github.com/nose7en/ToyBoomServer/defs"
"github.com/nose7en/ToyBoomServer/models"
)
func CreateUser(c context.Context, req *defs.CommonRequest) (*defs.CommonResponse, error) {
userInfo := common.GetUser(c)
newUser := &models.User{}
newUser.FillWithUserInfo(userInfo)
if err := dao.NewMutation().CreateUser(newUser); err != nil {
return nil, err
}
return &defs.CommonResponse{
Status: &defs.Status{Code: defs.RespCode_SUCCESS, Message: defs.RespMessage_SUCCESS},
}, nil
}

16
biz/user/get.go Normal file
View File

@ -0,0 +1,16 @@
package user
import (
"context"
"github.com/nose7en/ToyBoomServer/common"
"github.com/nose7en/ToyBoomServer/defs"
"github.com/samber/lo"
)
func GetUserInfo(c context.Context, req *defs.CommonRequest) (resp *defs.GetUserInfoResponse, err error) {
userInfo := common.GetUser(c)
return &defs.GetUserInfoResponse{
User: lo.ToPtr(userInfo.ToUser()),
}, nil
}

50
cmd/db.go Normal file
View File

@ -0,0 +1,50 @@
package main
import (
"context"
"github.com/nose7en/ToyBoomServer/common"
"github.com/nose7en/ToyBoomServer/config"
"github.com/nose7en/ToyBoomServer/models"
"github.com/nose7en/ToyBoomServer/storage"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func initDatabase() {
c := context.Background()
common.Logger(c).Infof("start to init database, type: %s", config.GetSettings().DB.Type)
storage.MustInitDBManager(nil, config.GetSettings().DB.Type)
switch config.GetSettings().DB.Type {
case "sqlite":
if sqlitedb, err := gorm.Open(sqlite.Open(config.GetSettings().DB.DSN), &gorm.Config{}); err != nil {
common.Logger(c).Panic(err)
} else {
storage.GetDBManager().SetDB("sqlite", storage.DefaultDBName, sqlitedb)
common.Logger(c).Infof("init database success, data location: [%s]", config.GetSettings().DB.DSN)
}
case "mysql":
if mysqlDB, err := gorm.Open(mysql.Open(config.GetSettings().DB.DSN), &gorm.Config{}); err != nil {
common.Logger(c).Panic(err)
} else {
storage.GetDBManager().SetDB("mysql", storage.DefaultDBName, mysqlDB)
common.Logger(c).Infof("init database success, data type: [%s]", "mysql")
}
case "postgres":
if postgresDB, err := gorm.Open(postgres.Open(config.GetSettings().DB.DSN), &gorm.Config{}); err != nil {
common.Logger(c).Panic(err)
} else {
storage.GetDBManager().SetDB("postgres", storage.DefaultDBName, postgresDB)
common.Logger(c).Infof("init database success, data type: [%s]", "postgres")
}
default:
common.Logger(c).Panicf("currently unsupported database type: %s", config.GetSettings().DB.Type)
}
storage.GetDBManager().Init(models.Models()...)
}

47
cmd/main.go Normal file
View File

@ -0,0 +1,47 @@
package main
import (
"context"
"github.com/nose7en/ToyBoomServer/biz"
"github.com/nose7en/ToyBoomServer/common"
"github.com/nose7en/ToyBoomServer/config"
"github.com/nose7en/ToyBoomServer/middleware"
"github.com/nose7en/ToyBoomServer/rpc"
"github.com/nose7en/ToyBoomServer/services/api"
"github.com/sourcegraph/conc"
)
// go:embed all:out/*
// var fs embed.FS
func main() {
c := context.Background()
config.InitSettings()
rpc.InitManager()
middleware.Init()
initDatabase()
// ------------ biz init ----------------
// --------------------------------------
r := biz.Router()
// biz.HandleStaticFile(r, fs, ar)
apiService := api.NewAPIHandler(config.GetSettings().APIListenAddr)
apiService.Init(r)
// watherService := watcher.NewClient(func() error {
// return nil
// })
common.Logger(c).Infof("------------------------------------------------")
common.Logger(c).Infof("init services done, start to run all services...")
common.Logger(c).Infof("------------------------------------------------")
var wg conc.WaitGroup
wg.Go(apiService.Run)
// wg.Go(watherService.Run)
wg.Wait()
}

62
common/const.go Normal file
View File

@ -0,0 +1,62 @@
package common
import "time"
const (
AuthorizationKey = "authorization"
SetAuthorizationKey = "x-set-authorization"
MsgKey = "msg"
DataKey = "data"
UserIDKey = "x-toyboom-userid"
UserNameKey = "x-toyboom-username"
CommandKey = "x-toyboom-command"
EndpointKey = "endpoint"
IpAddrKey = "ipaddr"
HeaderKey = "header"
MethodKey = "method"
UAKey = "User-Agent"
ContentTypeKey = "Content-Type"
TraceIDKey = "TraceID"
TokenKey = "token"
ErrKey = "err"
UserInfoKey = "x-toyboom-userinfo"
HostKey = "Host"
LogtoWebHookSignKey = "Logto-Signature-Sha-256"
NormalUIDKey = "uid"
)
const (
ErrInvalidRequest = "invalid request"
ErrUserInfoNotValid = "user info not valid"
ErrInternalError = "internal error"
ErrParamNotValid = "param not valid"
ErrDB = "database error"
ErrNotFound = "data not found"
ErrCodeNotFound = "code not found"
ErrCodeAlreadyUsed = "code already used"
)
const (
ReqSuccess = "success"
)
const (
TimeLayout = time.RFC3339
)
const (
StatusPending = "pending"
StatusSuccess = "success"
StatusFailed = "failed"
StatusDone = "done"
)
const (
CliTypeServer = "server"
CliTypeClient = "client"
)
const (
DefaultServerID = "default"
DefaultAdminUserID = 1
)

21
common/context.go Normal file
View File

@ -0,0 +1,21 @@
package common
import (
"context"
"github.com/nose7en/ToyBoomServer/defs"
)
func GetUser(c context.Context) defs.UserGettable {
val := c.Value(UserInfoKey)
if val == nil {
return nil
}
u, ok := val.(defs.UserGettable)
if !ok {
return nil
}
return u
}

38
common/helper.go Normal file
View File

@ -0,0 +1,38 @@
package common
import (
"context"
"fmt"
"github.com/nose7en/ToyBoomServer/defs"
"github.com/gin-gonic/gin"
)
func GlobalClientID(username, clientType, clientID string) string {
return fmt.Sprintf("%s.%s.%s", username, clientType, clientID)
}
func Wrapper[T ReqType, U RespType](handler func(context.Context, *T) (*U, error)) func(c *gin.Context) {
return func(c *gin.Context) {
Logger(c).Infof("request url: %s", c.Request.URL)
req, err := GetProtoRequest[T](c)
if err != nil {
Logger(c).WithError(err).Errorf("Failed to get request, url: %s", c.Request.URL)
ErrResp(c, &defs.CommonResponse{
Status: &defs.Status{Code: defs.RespCode_INVALID, Message: defs.RespMessage_INVALID},
}, err.Error())
return
}
resp, err := handler(c, req)
if err != nil {
Logger(c).WithError(err).Errorf("Failed to handle request, url: %s", c.Request.URL)
ErrResp(c, resp, err.Error())
return
}
OKResp(c, resp)
Logger(c).Infof("handle request success, response url: %s", c.Request.URL)
}
}

34
common/logger.go Normal file
View File

@ -0,0 +1,34 @@
package common
import (
"context"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
func init() {
logrus.AddHook(&ObserveHook{})
}
func Logger(c context.Context) *logrus.Entry {
var uid, endpoint, header, method, traceid interface{}
if c != nil {
ginCtx, ok := c.(*gin.Context)
if ok {
uid = ginCtx.GetString(UserIDKey)
endpoint = ginCtx.Request.URL.Path
header = ginCtx.Request.Header
method = ginCtx.Request.Method
traceid = ginCtx.GetString(TraceIDKey)
}
}
logrus.SetFormatter(&logrus.JSONFormatter{})
return logrus.WithContext(c).
WithField(UserIDKey, uid).
WithField(EndpointKey, endpoint).
WithField(HeaderKey, header).
WithField(MethodKey, method).
WithField(TraceIDKey, traceid)
}

65
common/logger_remote.go Normal file
View File

@ -0,0 +1,65 @@
package common
import (
"encoding/json"
"fmt"
"github.com/sirupsen/logrus"
)
type ObserveHook struct{}
func (h *ObserveHook) Levels() []logrus.Level {
return []logrus.Level{
logrus.ErrorLevel,
logrus.PanicLevel,
logrus.InfoLevel,
}
}
func (h *ObserveHook) Fire(entry *logrus.Entry) error {
go func() {
data := []map[string]interface{}{{
"_msg": entry.Message,
"_level": entry.Level,
"_time": entry.Time,
"_app": "sharkapi",
"_userid": entry.Data[UserIDKey],
"_endpoint": entry.Data[EndpointKey],
"_header": entry.Data[HeaderKey],
"_x_trace_id": entry.Data[TraceIDKey],
}}
if e, ok := entry.Data[logrus.ErrorKey].(error); ok {
data[0]["_err"] = e.Error()
}
c, err := json.Marshal(data)
if err != nil {
return
}
err = log2openOb(c)
if err != nil {
fmt.Printf("log2openOb error: %v", err.Error())
}
}()
return nil
}
func log2openOb(l []byte) error {
return nil
// cli := req.C() //.DevMode()
// resp, err := cli.R().SetBasicAuth(config.GetSettings().LogUser,
// config.GetSettings().LogKey).
// SetBody(l).
// SetHeader("Content-Type", "application/json").
// Post(config.GetSettings().LogEndpoint)
// if err != nil {
// return err
// }
// if resp.Response == nil {
// return errors.New("resp err")
// }
// return nil
}

34
common/request.go Normal file
View File

@ -0,0 +1,34 @@
package common
import (
"github.com/nose7en/ToyBoomServer/defs"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
type ReqType interface {
gin.H |
defs.CommonPaginationRequest | defs.CommonQueryPaginationRequest |
defs.CommonQueryRequest | defs.CommonRequest
}
func GetProtoRequest[T ReqType](c *gin.Context) (r *T, err error) {
r = new(T)
if c.Request.ContentLength == 0 {
return r, nil
}
if c.ContentType() == "application/x-protobuf" {
err = c.Copy().ShouldBindWith(r, binding.ProtoBuf)
if err != nil {
return nil, err
}
} else {
err = c.Copy().ShouldBindJSON(r)
if err != nil {
return nil, err
}
}
return r, nil
}

43
common/response.go Normal file
View File

@ -0,0 +1,43 @@
package common
import (
"net/http"
"github.com/nose7en/ToyBoomServer/defs"
"github.com/gin-gonic/gin"
)
type RespType interface {
gin.H |
defs.CommonResponse | defs.GetUserInfoResponse |
defs.GetUserAuthTokenResponse
}
func OKResp[T RespType](c *gin.Context, origin *T) {
c.Header(TraceIDKey, c.GetString(TraceIDKey))
if c.ContentType() == "application/x-protobuf" {
c.ProtoBuf(http.StatusOK, origin)
} else {
c.JSON(http.StatusOK, OK(ReqSuccess).WithBody(origin))
}
}
func ErrResp[T RespType](c *gin.Context, origin *T, err string) {
c.Header(TraceIDKey, c.GetString(TraceIDKey))
if c.ContentType() == "application/x-protobuf" {
c.ProtoBuf(http.StatusInternalServerError, origin)
} else {
c.JSON(http.StatusOK, Err(err).WithBody(origin))
}
}
func ErrUnAuthorized(c *gin.Context, err string) {
c.Header(TraceIDKey, c.GetString(TraceIDKey))
if c.ContentType() == "application/x-protobuf" {
c.ProtoBuf(http.StatusUnauthorized,
&defs.CommonResponse{Status: &defs.Status{Code: defs.RespCode_UNAUTHORIZED, Message: defs.RespMessage_UNAUTHORIZED}})
} else {
c.JSON(http.StatusOK, Err(err))
}
}

55
common/result.go Normal file
View File

@ -0,0 +1,55 @@
package common
import (
"github.com/gin-gonic/gin"
)
type Result struct {
Code int `json:"code,omitempty"`
Msg string `json:"msg,omitempty"`
Data gin.H `json:"data,omitempty"`
Body interface{} `json:"body,omitempty"`
}
func (r *Result) WithMsg(message string) *Result {
r.Msg = message
return r
}
func (r *Result) WithData(data gin.H) *Result {
r.Data = data
return r
}
func (r *Result) WithKeyValue(key string, value interface{}) *Result {
if r.Data == nil {
r.Data = gin.H{}
}
r.Data[key] = value
return r
}
func (r *Result) WithBody(body interface{}) *Result {
r.Body = body
return r
}
func newResult(code int, msg string) *Result {
return &Result{
Code: code,
Msg: msg,
Data: nil,
}
}
func OK(msg string) *Result {
return newResult(200, msg)
}
func Err(msg string) *Result {
return newResult(500, msg)
}
func UnAuth(msg string) *Result {
return newResult(401, msg)
}

17
components.json Normal file
View File

@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

5
config/helper.go Normal file
View File

@ -0,0 +1,5 @@
package config
func IsDebug() bool {
return GetSettings().Debug
}

95
config/settings.go Normal file
View File

@ -0,0 +1,95 @@
package config
import (
"context"
"strings"
"github.com/nose7en/ToyBoomServer/common"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
type Settings struct {
Debug bool `mapstructure:"debug"`
APIListenAddr string `mapstructure:"api_addr"`
RedisConf RedisConf `mapstructure:"redis_conf"`
CacheSize int `mapstructure:"cache_size"` // MB
DB DBConf `mapstructure:"db"`
JWTConfig JWTConfig `mapstructure:"jwt"`
AppleCfg AppleConf `mapstructure:"apple"`
}
type DBConf struct {
Type string `mapstructure:"type"`
DSN string `mapstructure:"dsn"`
}
type RedisConf struct {
Addr string
Password string
DB int
}
type JWTConfig struct {
Secret string `mapstructure:"secret"`
}
type AppleConf struct {
Secret string `mapstructure:"secret"`
TeamID string `mapstructure:"team_id"`
ClientID string `mapstructure:"client_id"`
KeyID string `mapstructure:"key_id"`
}
var settings *Settings
func GetSettings() *Settings {
return settings
}
func InitSettings() {
c := context.Background()
setConfigParams()
fillDefaultSettings()
readAndParseConfig(c)
}
func fillDefaultSettings() {
viper.SetDefault("cache_size", 10)
viper.SetDefault("debug", false)
viper.SetDefault("db.type", "sqlite")
viper.SetDefault("db.dsn", "toyboom.db")
}
func setConfigParams() {
viper.SetConfigName("config")
viper.SetConfigType("yml")
viper.AddConfigPath("/etc/toyboom/")
viper.AddConfigPath("$HOME/.toyboom")
viper.AddConfigPath(".")
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
}
func readAndParseConfig(c context.Context) {
err := viper.ReadInConfig()
if err != nil {
common.Logger(c).WithError(err).Errorf("Error reading config file:[%s], will read from env", viper.ConfigFileUsed())
}
err = viper.Unmarshal(&settings)
if err != nil {
common.Logger(c).Panic(err)
}
if IsDebug() {
logrus.SetLevel(logrus.DebugLevel)
gin.SetMode(gin.DebugMode)
} else {
logrus.SetLevel(logrus.InfoLevel)
gin.SetMode(gin.ReleaseMode)
}
}

27
dao/interface.go Normal file
View File

@ -0,0 +1,27 @@
package dao
import (
"github.com/nose7en/ToyBoomServer/defs"
)
type Query interface {
GetUserByAppleUserID(appleUserID string) (defs.UserGettable, error)
}
type Mutation interface {
CreateUser(user defs.UserGettable) error
}
var _ Query = (*queryImpl)(nil)
var _ Mutation = (*mutationImpl)(nil)
type queryImpl struct{}
type mutationImpl struct{}
func NewQuery() Query {
return &queryImpl{}
}
func NewMutation() Mutation {
return &mutationImpl{}
}

13
dao/user.go Normal file
View File

@ -0,0 +1,13 @@
package dao
import "github.com/nose7en/ToyBoomServer/defs"
func (q *queryImpl) GetUserByAppleUserID(appleUserID string) (defs.UserGettable, error) {
return nil, nil
}
func (m *mutationImpl) CreateUser(user defs.UserGettable) error {
return nil
}

17
defs/entity.go Normal file
View File

@ -0,0 +1,17 @@
package defs
type User struct {
UserID string `json:"user_id"`
Name string `json:"name"`
Username string `json:"username"`
Email string `json:"email"`
IsPrivateEmail bool `json:"is_private_email"`
EmailVerified bool `json:"email_verified"`
// Tenants []Tenant
}
// type Tenant struct {
// ID string `json:"id"`
// Name string `json:"name"`
// Description string `json:"description"`
// }

23
defs/request.go Normal file
View File

@ -0,0 +1,23 @@
package defs
type CommonRequest struct {
Extra interface{} `json:"extra,omitempty"`
}
type CommonPaginationRequest struct {
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
Extra interface{} `json:"extra,omitempty"`
}
type CommonQueryRequest struct {
Query string `json:"query,omitempty"`
Extra interface{} `json:"extra,omitempty"`
}
type CommonQueryPaginationRequest struct {
Query string `json:"query,omitempty"`
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
Extra interface{} `json:"extra,omitempty"`
}

37
defs/response.go Normal file
View File

@ -0,0 +1,37 @@
package defs
const (
RespCode_SUCCESS = RespCode(0)
RespCode_ERROR = RespCode(1)
RespCode_UNAUTHORIZED = RespCode(2)
RespCode_INVALID = RespCode(3)
)
const (
RespMessage_SUCCESS = RespMsg("success")
RespMessage_ERROR = RespMsg("error")
RespMessage_UNAUTHORIZED = RespMsg("unauthorized")
RespMessage_INVALID = RespMsg("invalid")
)
type RespCode int
type RespMsg string
type Status struct {
Code RespCode `json:"code"`
Message RespMsg `json:"message"`
}
type CommonResponse struct {
Status *Status `json:"status"`
}
type GetUserInfoResponse struct {
Status *Status `json:"status"`
User *User `json:"user"`
}
type GetUserAuthTokenResponse struct {
Status *Status `json:"status"`
Token string `json:"token"`
}

11
defs/user_info.go Normal file
View File

@ -0,0 +1,11 @@
package defs
type UserGettable interface {
GetUserID() string
GetName() string
GetUsername() string
GetEmail() string
GetIsPrivateEmail() bool
GetEmailVerified() bool
ToUser() User
}

86
go.mod Normal file
View File

@ -0,0 +1,86 @@
module github.com/nose7en/ToyBoomServer
go 1.21.5
require (
github.com/coocood/freecache v1.2.4
github.com/gin-contrib/sessions v0.0.5
github.com/gin-contrib/static v0.0.1
github.com/gin-gonic/gin v1.9.1
github.com/glebarez/sqlite v1.10.0
github.com/go-co-op/gocron/v2 v2.2.4
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/redis/go-redis/v9 v9.4.0
github.com/samber/lo v1.39.0
github.com/sirupsen/logrus v1.9.3
github.com/sourcegraph/conc v0.3.0
github.com/spf13/viper v1.18.2
gorm.io/driver/mysql v1.5.4
gorm.io/driver/postgres v1.5.6
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde
)
require (
github.com/Timothylock/go-signin-with-apple v0.2.3 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.4.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
)

228
go.sum Normal file
View File

@ -0,0 +1,228 @@
github.com/Timothylock/go-signin-with-apple v0.2.3 h1:t8y3cVW/L5+s4RSaWn4NC5VToLqZDpwkLsrcm9bGhhE=
github.com/Timothylock/go-signin-with-apple v0.2.3/go.mod h1:drDUeawmKp2eRdbXh99+5HKbGT1O1wLA+1LNN4+cs9w=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc=
github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA=
github.com/go-co-op/gocron/v2 v2.2.4 h1:fL6a8/U+BJQ9UbaeqKxua8wY02w4ftKZsxPzLSNOCKk=
github.com/go-co-op/gocron/v2 v2.2.4/go.mod h1:igssOwzZkfcnu3m2kwnCf/mYj4SmhP9ecSgmYjCOHkk=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 h1:dHLYa5D8/Ta0aLR2XcPsrkpAgGeFs6thhMcQK0oQ0n8=
github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM=
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.4 h1:igQmHfKcbaTVyAIHNhhB888vvxh8EdQ2uSUT0LPcBso=
gorm.io/driver/mysql v1.5.4/go.mod h1:9rYxJph/u9SWkWc9yY4XJ1F/+xO0S/ChOmbk3+Z5Tvs=
gorm.io/driver/postgres v1.5.6 h1:ydr9xEd5YAM0vxVDY0X139dyzNz10spDiDlC7+ibLeU=
gorm.io/driver/postgres v1.5.6/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde h1:9DShaph9qhkIYw7QF91I/ynrr4cOO2PZra2PFD7Mfeg=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

59
middleware/auth.go Normal file
View File

@ -0,0 +1,59 @@
package middleware
import (
"net/http"
"github.com/Timothylock/go-signin-with-apple/apple"
"github.com/nose7en/ToyBoomServer/common"
"github.com/nose7en/ToyBoomServer/config"
"github.com/nose7en/ToyBoomServer/defs"
"github.com/nose7en/ToyBoomServer/rpc"
"github.com/spf13/cast"
"github.com/gin-gonic/gin"
)
func ValidateAppleAppToken() func(c *gin.Context) {
return func(c *gin.Context) {
code := c.GetHeader(common.TokenKey)
resp, err := rpc.GetManager().AppleCli().VerifyAppToken(c, code)
if err != nil || len(resp.Error) > 0 {
common.Logger(c).WithError(err).Errorf("failed to verify apple token, response error: %s", resp.Error)
c.AbortWithStatusJSON(http.StatusOK, common.UnAuth("failed to verify apple token"))
return
}
// Get the unique user ID
unique, err := apple.GetUniqueID(resp.IDToken)
if err != nil {
common.Logger(c).WithError(err).Error("failed to get apple unique id")
c.AbortWithStatusJSON(http.StatusOK, common.UnAuth("failed to verify apple token"))
return
}
// Get detail user info
claim, err := apple.GetClaims(resp.IDToken)
if err != nil || claim == nil {
common.Logger(c).WithError(err).Error("failed to get apple user info or claim is nil")
c.AbortWithStatusJSON(http.StatusOK, common.UnAuth("failed to verify apple token"))
return
}
if config.IsDebug() {
common.Logger(c).Debugf("apple auth success, user info: %+v", claim)
}
email := cast.ToString((*claim)["email"])
emailVerified := cast.ToBool((*claim)["email_verified"])
isPrivateEmail := cast.ToBool((*claim)["is_private_email"])
userInfo := &defs.User{
UserID: unique,
Email: email,
IsPrivateEmail: isPrivateEmail,
EmailVerified: emailVerified,
}
common.Logger(c).Infof("apple auth success, user info: %+v", userInfo)
c.Set(common.UserInfoKey, userInfo)
}
}

4
middleware/init.go Normal file
View File

@ -0,0 +1,4 @@
package middleware
func Init() {
}

7
models/models.go Normal file
View File

@ -0,0 +1,7 @@
package models
func Models() []interface{} {
return []interface{}{
&User{},
}
}

68
models/user.go Normal file
View File

@ -0,0 +1,68 @@
package models
import (
"github.com/nose7en/ToyBoomServer/defs"
"gorm.io/gorm"
)
var _ defs.UserGettable = (*User)(nil)
func NewUserGettable(opt ...func(*User)) defs.UserGettable {
u := &User{}
for _, o := range opt {
o(u)
}
return u
}
type User struct {
gorm.Model
UserID string `json:"user_id"` // user id from apple
Name string `json:"name"` // ToyBoom's user name
Username string `json:"username"` // user name from apple
Email string `json:"email"` // email from apple
IsPrivateEmail bool `json:"is_private_email"`
EmailVerified bool `json:"email_verified"`
}
func (u *User) FillWithUserInfo(userInfo defs.UserGettable) {
u.UserID = userInfo.GetUserID()
u.Name = userInfo.GetName()
u.Username = userInfo.GetUsername()
u.Email = userInfo.GetEmail()
u.IsPrivateEmail = userInfo.GetIsPrivateEmail()
u.EmailVerified = userInfo.GetEmailVerified()
}
func (u *User) GetName() string {
return u.Name
}
func (u *User) GetUsername() string {
return u.Username
}
func (u *User) GetUserID() string {
return u.UserID
}
func (u *User) GetEmail() string {
return u.Email
}
func (u *User) GetIsPrivateEmail() bool {
return u.IsPrivateEmail
}
func (u *User) GetEmailVerified() bool {
return u.EmailVerified
}
func (u *User) ToUser() defs.User {
return defs.User{
UserID: u.UserID,
Name: u.Name,
Username: u.Username,
Email: u.Email,
}
}

16
next.config.mjs Normal file
View File

@ -0,0 +1,16 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
async rewrites() {
return {
fallback: [
{
source: '/api/:path*',
destination: 'http://localhost:8080/api/:path*',
}
]
}
},
};
export default nextConfig;

60
package.json Normal file
View File

@ -0,0 +1,60 @@
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build && rm -rf cmd/out && cp -r out cmd/",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@nanostores/react": "^0.7.1",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.18.1",
"ai": "^2.2.33",
"axios": "^1.6.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-react": "^0.323.0",
"nanoid": "^5.0.5",
"nanostores": "^0.9.5",
"next": "14.1.0",
"next-themes": "^0.2.1",
"react": "^18",
"react-dom": "^18",
"react-intersection-observer": "^9.7.0",
"react-markdown": "^8.0.7",
"react-spinners": "^0.13.8",
"react-syntax-highlighter": "^15.5.0",
"react-textarea-autosize": "^8.5.3",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"sonner": "^1.4.0",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"use-local-storage": "^3.0.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-syntax-highlighter": "^15.5.11",
"@types/uuid": "^9.0.8",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}

5671
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

1
public/bg.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='8' height='8' fill='none' stroke='rgb(0 0 0 / 0.1)'><path d='M0 .5H31.5V32'/></svg>

After

Width:  |  Height:  |  Size: 150 B

1
public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

85
rpc/apple.go Normal file
View File

@ -0,0 +1,85 @@
package rpc
import (
"context"
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"errors"
"time"
"github.com/Timothylock/go-signin-with-apple/apple"
"github.com/golang-jwt/jwt/v5"
)
type AppleClient struct {
*apple.Client
teamID string
clientID string
keyID string
secret string
}
// GenerateClientSecret generates a new client secret, all fields are required, no http request, fully local logic
func (a *AppleClient) GenerateClientSecret() (string, error) {
token := &jwt.Token{
Header: map[string]interface{}{
"alg": "ES256",
"kid": a.keyID,
},
Claims: jwt.MapClaims{
"iss": a.teamID,
"iat": time.Now().Unix(),
// constraint: exp - iat <= 180 days
"exp": time.Now().Add(24 * time.Hour).Unix(),
"aud": "https://appleid.apple.com",
"sub": a.clientID,
},
Method: jwt.SigningMethodES256,
}
ecdsaKey, _ := authKeyFromBytes([]byte(a.secret))
return token.SignedString(ecdsaKey)
}
// authKeyFromBytes create private key for jwt sign
func authKeyFromBytes(key []byte) (*ecdsa.PrivateKey, error) {
var err error
var block *pem.Block
if block, _ = pem.Decode(key); block == nil {
return nil, errors.New("token: AuthKey must be a valid .p8 PEM file")
}
var parsedKey interface{}
if parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil {
return nil, err
}
var pkey *ecdsa.PrivateKey
var ok bool
if pkey, ok = parsedKey.(*ecdsa.PrivateKey); !ok {
return nil, errors.New("token: AuthKey must be of type ecdsa.PrivateKey")
}
return pkey, nil
}
func (a *AppleClient) VerifyAppToken(ctx context.Context, code string) (apple.ValidationResponse, error) {
var resp apple.ValidationResponse
clientSecret, err := a.GenerateClientSecret()
if err != nil {
return resp, err
}
reqBody := apple.AppValidationTokenRequest{
ClientID: a.clientID,
ClientSecret: clientSecret,
Code: code,
}
if err := a.Client.VerifyAppToken(ctx, reqBody, &resp); err != nil {
return resp, err
}
return resp, nil
}

23
rpc/apple_test.go Normal file
View File

@ -0,0 +1,23 @@
package rpc
import (
"context"
"testing"
"github.com/nose7en/ToyBoomServer/config"
)
const token = "c2fa05395f0974b798d7d9ae802a17bae.0.srvuz.8c3w4pp04rFe9CXbrtNxVg"
func TestAppleClient_VerifyAppToken(t *testing.T) {
config.InitSettings()
InitManager()
cli := GetManager().AppleCli()
resp, err := cli.VerifyAppToken(context.Background(), token)
t.Error(err)
t.Error(resp)
}

40
rpc/manager.go Normal file
View File

@ -0,0 +1,40 @@
package rpc
import (
"github.com/Timothylock/go-signin-with-apple/apple"
"github.com/nose7en/ToyBoomServer/config"
)
type Manager interface {
AppleCli() *AppleClient
}
var (
mgr Manager = (*manager)(nil)
)
func GetManager() Manager {
return mgr
}
func InitManager() {
mgr = &manager{}
}
type manager struct {
appleCli *AppleClient
}
func (m *manager) AppleCli() *AppleClient {
if m.appleCli == nil {
m.appleCli = &AppleClient{
Client: apple.New(),
teamID: config.GetSettings().AppleCfg.TeamID,
clientID: config.GetSettings().AppleCfg.ClientID,
keyID: config.GetSettings().AppleCfg.KeyID,
secret: config.GetSettings().AppleCfg.Secret,
}
}
return m.appleCli
}

28
services/api/handler.go Normal file
View File

@ -0,0 +1,28 @@
package api
import (
"github.com/gin-gonic/gin"
)
type APIHandler struct {
router *gin.Engine
addr []string
}
func NewAPIHandler(addr ...string) *APIHandler {
return &APIHandler{
addr: addr,
}
}
func (h *APIHandler) Init(r *gin.Engine) {
h.router = r
}
func (h *APIHandler) Run() {
h.router.Run(h.addr...)
}
func (h *APIHandler) GetRouter() *gin.Engine {
return h.router
}

21
src/api/ai.ts Normal file
View File

@ -0,0 +1,21 @@
import { type Message } from 'ai/react'
import api from './api'
import { ChatShareAPIPrefix } from '@/const/api';
export interface ChatShareRequest {
chatId: string
}
export interface ChatShareResp {
userID: number;
id: string;
messages: Message[];
}
export const getSharedChat = async (id: string) => {
const res = await api.post(ChatShareAPIPrefix + '/v1/chat/messages/share',
{ chatId: id } as ChatShareRequest,
)
console.log("getSharedChat api response: ", res.data)
return res.data as ChatShareResp
}

16
src/api/api.ts Normal file
View File

@ -0,0 +1,16 @@
"use client"
import axios from 'axios'
import { v4 as uuidv4 } from 'uuid';
const instance = axios.create({})
instance.interceptors.request.use((request) => {
request.headers["x-client-request-id"] = uuidv4();
return request
})
instance.interceptors.response.use((response) => {
return response
})
export default instance

10
src/api/apigw.ts Normal file
View File

@ -0,0 +1,10 @@
"use client"
import api from "./api"
import { APIPrefix } from "@/const/api"
export const checkstatus = async () => {
console.log("checkstatus")
const res = await api.get(APIPrefix + '/ping')
return res
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

76
src/app/globals.css Normal file
View File

@ -0,0 +1,76 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

24
src/app/layout.tsx Normal file
View File

@ -0,0 +1,24 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "VaalaAI",
description: "Vaala的AI",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
</html>
);
}

7
src/app/page.tsx Normal file
View File

@ -0,0 +1,7 @@
export default function Home() {
return (
<main>
hello
</main >
)
}

View File

@ -0,0 +1,12 @@
"use client"
import { TooltipProvider } from '@radix-ui/react-tooltip';
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient()
export default function Providers({ children }: { children: React.ReactNode }) {
return <TooltipProvider>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</TooltipProvider>
}

View File

@ -0,0 +1,141 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,148 @@
// Inspired by Chatbot-UI and modified to fit the needs of this project
// @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Markdown/CodeBlock.tsx
'use client'
import { FC, memo } from 'react'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
import { IconCheck, IconCopy, IconDownload } from '@/components/ui/icons'
import { Button } from '@/components/ui/button'
import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
interface Props {
language: string
value: string
}
interface languageMap {
[key: string]: string | undefined
}
export const programmingLanguages: languageMap = {
javascript: '.js',
python: '.py',
java: '.java',
c: '.c',
cpp: '.cpp',
'c++': '.cpp',
'c#': '.cs',
ruby: '.rb',
php: '.php',
swift: '.swift',
'objective-c': '.m',
kotlin: '.kt',
typescript: '.ts',
go: '.go',
perl: '.pl',
rust: '.rs',
scala: '.scala',
haskell: '.hs',
lua: '.lua',
shell: '.sh',
sql: '.sql',
html: '.html',
css: '.css'
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
}
export const generateRandomString = (length: number, lowercase = false) => {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' // excluding similar looking characters like Z, 2, I, 1, O, 0
let result = ''
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return lowercase ? result.toLowerCase() : result
}
const CodeBlock: FC<Props> = memo(({ language, value }) => {
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
const downloadAsFile = () => {
if (typeof window === 'undefined') {
return
}
const fileExtension = programmingLanguages[language] || '.file'
const suggestedFileName = `file-${generateRandomString(
3,
true
)}${fileExtension}`
const fileName = window.prompt('Enter file name' || '', suggestedFileName)
if (!fileName) {
// User pressed cancel on prompt.
return
}
const blob = new Blob([value], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.download = fileName
link.href = url
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
const onCopy = () => {
if (isCopied) return
copyToClipboard(value)
}
return (
<div className="relative w-full font-sans codeblock bg-zinc-950">
<div className="flex items-center justify-between w-full px-6 py-2 pr-4 bg-zinc-800 text-zinc-100">
<span className="text-xs lowercase">{language}</span>
<div className="flex items-center space-x-1">
<Button
variant="ghost"
className="hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
onClick={downloadAsFile}
size="icon"
>
<IconDownload />
<span className="sr-only">Download</span>
</Button>
<Button
variant="ghost"
size="icon"
className="text-xs hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
onClick={onCopy}
>
{isCopied ? <IconCheck /> : <IconCopy />}
<span className="sr-only">Copy code</span>
</Button>
</div>
</div>
<SyntaxHighlighter
language={language}
style={coldarkDark}
PreTag="div"
showLineNumbers
customStyle={{
margin: 0,
width: '100%',
background: 'transparent',
padding: '1.5rem 1rem'
}}
lineNumberStyle={{
userSelect: "none",
}}
codeTagProps={{
style: {
fontSize: '0.9rem',
fontFamily: 'var(--font-mono)'
}
}}
>
{value}
</SyntaxHighlighter>
</div>
)
})
CodeBlock.displayName = 'CodeBlock'
export { CodeBlock }

View File

@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
const HoverCard = HoverCardPrimitive.Root
const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardTrigger, HoverCardContent }

507
src/components/ui/icons.tsx Normal file
View File

@ -0,0 +1,507 @@
'use client'
import * as React from 'react'
import { cn } from '@/lib/utils'
function IconNextChat({
className,
inverted,
...props
}: React.ComponentProps<'svg'> & { inverted?: boolean }) {
const id = React.useId()
return (
<svg
viewBox="0 0 17 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={cn('h-4 w-4', className)}
{...props}
>
<defs>
<linearGradient
id={`gradient-${id}-1`}
x1="10.6889"
y1="10.3556"
x2="13.8445"
y2="14.2667"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={inverted ? 'white' : 'black'} />
<stop
offset={1}
stopColor={inverted ? 'white' : 'black'}
stopOpacity={0}
/>
</linearGradient>
<linearGradient
id={`gradient-${id}-2`}
x1="11.7555"
y1="4.8"
x2="11.7376"
y2="9.50002"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={inverted ? 'white' : 'black'} />
<stop
offset={1}
stopColor={inverted ? 'white' : 'black'}
stopOpacity={0}
/>
</linearGradient>
</defs>
<path
d="M1 16L2.58314 11.2506C1.83084 9.74642 1.63835 8.02363 2.04013 6.39052C2.4419 4.75741 3.41171 3.32057 4.776 2.33712C6.1403 1.35367 7.81003 0.887808 9.4864 1.02289C11.1628 1.15798 12.7364 1.8852 13.9256 3.07442C15.1148 4.26363 15.842 5.83723 15.9771 7.5136C16.1122 9.18997 15.6463 10.8597 14.6629 12.224C13.6794 13.5883 12.2426 14.5581 10.6095 14.9599C8.97637 15.3616 7.25358 15.1692 5.74942 14.4169L1 16Z"
fill={inverted ? 'black' : 'white'}
stroke={inverted ? 'black' : 'white'}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
<mask
id="mask0_91_2047"
style={{ maskType: 'alpha' }}
maskUnits="userSpaceOnUse"
x={1}
y={0}
width={16}
height={16}
>
<circle cx={9} cy={8} r={8} fill={inverted ? 'black' : 'white'} />
</mask>
<g mask="url(#mask0_91_2047)">
<circle cx={9} cy={8} r={8} fill={inverted ? 'black' : 'white'} />
<path
d="M14.2896 14.0018L7.146 4.8H5.80005V11.1973H6.87681V6.16743L13.4444 14.6529C13.7407 14.4545 14.0231 14.2369 14.2896 14.0018Z"
fill={`url(#gradient-${id}-1)`}
/>
<rect
x="11.2222"
y="4.8"
width="1.06667"
height="6.4"
fill={`url(#gradient-${id}-2)`}
/>
</g>
</svg>
)
}
function IconOpenAI({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
fill="currentColor"
viewBox="0 0 24 24"
role="img"
xmlns="http://www.w3.org/2000/svg"
className={cn('h-4 w-4', className)}
{...props}
>
<title>OpenAI icon</title>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
)
}
function IconVercel({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
aria-label="Vercel logomark"
role="img"
viewBox="0 0 74 64"
className={cn('h-4 w-4', className)}
{...props}
>
<path
d="M37.5896 0.25L74.5396 64.25H0.639648L37.5896 0.25Z"
fill="currentColor"
></path>
</svg>
)
}
function IconGitHub({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
)
}
function IconSeparator({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
fill="none"
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1"
viewBox="0 0 24 24"
aria-hidden="true"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="M16.88 3.549L7.12 20.451"></path>
</svg>
)
}
function IconArrowDown({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="m205.66 149.66-72 72a8 8 0 0 1-11.32 0l-72-72a8 8 0 0 1 11.32-11.32L120 196.69V40a8 8 0 0 1 16 0v156.69l58.34-58.35a8 8 0 0 1 11.32 11.32Z" />
</svg>
)
}
function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="m221.66 133.66-72 72a8 8 0 0 1-11.32-11.32L196.69 136H40a8 8 0 0 1 0-16h156.69l-58.35-58.34a8 8 0 0 1 11.32-11.32l72 72a8 8 0 0 1 0 11.32Z" />
</svg>
)
}
function IconUser({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8ZM72 96a56 56 0 1 1 56 56 56.06 56.06 0 0 1-56-56Z" />
</svg>
)
}
function IconPlus({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="M224 128a8 8 0 0 1-8 8h-80v80a8 8 0 0 1-16 0v-80H40a8 8 0 0 1 0-16h80V40a8 8 0 0 1 16 0v80h80a8 8 0 0 1 8 8Z" />
</svg>
)
}
function IconArrowElbow({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="M200 32v144a8 8 0 0 1-8 8H67.31l34.35 34.34a8 8 0 0 1-11.32 11.32l-48-48a8 8 0 0 1 0-11.32l48-48a8 8 0 0 1 11.32 11.32L67.31 168H184V32a8 8 0 0 1 16 0Z" />
</svg>
)
}
function IconSpinner({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4 animate-spin', className)}
{...props}
>
<path d="M232 128a104 104 0 0 1-208 0c0-41 23.81-78.36 60.66-95.27a8 8 0 0 1 6.68 14.54C60.15 61.59 40 93.27 40 128a88 88 0 0 0 176 0c0-34.73-20.15-66.41-51.34-80.73a8 8 0 0 1 6.68-14.54C208.19 49.64 232 87 232 128Z" />
</svg>
)
}
function IconMessage({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="M216 48H40a16 16 0 0 0-16 16v160a15.84 15.84 0 0 0 9.25 14.5A16.05 16.05 0 0 0 40 240a15.89 15.89 0 0 0 10.25-3.78.69.69 0 0 0 .13-.11L82.5 208H216a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16ZM40 224Zm176-32H82.5a16 16 0 0 0-10.3 3.75l-.12.11L40 224V64h176Z" />
</svg>
)
}
function IconTrash({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="M216 48h-40v-8a24 24 0 0 0-24-24h-48a24 24 0 0 0-24 24v8H40a8 8 0 0 0 0 16h8v144a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16V64h8a8 8 0 0 0 0-16ZM96 40a8 8 0 0 1 8-8h48a8 8 0 0 1 8 8v8H96Zm96 168H64V64h128Zm-80-104v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0Zm48 0v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0Z" />
</svg>
)
}
function IconRefresh({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="M197.67 186.37a8 8 0 0 1 0 11.29C196.58 198.73 170.82 224 128 224c-37.39 0-64.53-22.4-80-39.85V208a8 8 0 0 1-16 0v-48a8 8 0 0 1 8-8h48a8 8 0 0 1 0 16H55.44C67.76 183.35 93 208 128 208c36 0 58.14-21.46 58.36-21.68a8 8 0 0 1 11.31.05ZM216 40a8 8 0 0 0-8 8v23.85C192.53 54.4 165.39 32 128 32c-42.82 0-68.58 25.27-69.66 26.34a8 8 0 0 0 11.3 11.34C69.86 69.46 92 48 128 48c35 0 60.24 24.65 72.56 40H168a8 8 0 0 0 0 16h48a8 8 0 0 0 8-8V48a8 8 0 0 0-8-8Z" />
</svg>
)
}
function IconStop({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24Zm0 192a88 88 0 1 1 88-88 88.1 88.1 0 0 1-88 88Zm24-120h-48a8 8 0 0 0-8 8v48a8 8 0 0 0 8 8h48a8 8 0 0 0 8-8v-48a8 8 0 0 0-8-8Zm-8 48h-32v-32h32Z" />
</svg>
)
}
function IconSidebar({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="M216 40H40a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16ZM40 56h40v144H40Zm176 144H96V56h120v144Z" />
</svg>
)
}
function IconMoon({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="M233.54 142.23a8 8 0 0 0-8-2 88.08 88.08 0 0 1-109.8-109.8 8 8 0 0 0-10-10 104.84 104.84 0 0 0-52.91 37A104 104 0 0 0 136 224a103.09 103.09 0 0 0 62.52-20.88 104.84 104.84 0 0 0 37-52.91 8 8 0 0 0-1.98-7.98Zm-44.64 48.11A88 88 0 0 1 65.66 67.11a89 89 0 0 1 31.4-26A106 106 0 0 0 96 56a104.11 104.11 0 0 0 104 104 106 106 0 0 0 14.92-1.06 89 89 0 0 1-26.02 31.4Z" />
</svg>
)
}
function IconSun({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="M120 40V16a8 8 0 0 1 16 0v24a8 8 0 0 1-16 0Zm72 88a64 64 0 1 1-64-64 64.07 64.07 0 0 1 64 64Zm-16 0a48 48 0 1 0-48 48 48.05 48.05 0 0 0 48-48ZM58.34 69.66a8 8 0 0 0 11.32-11.32l-16-16a8 8 0 0 0-11.32 11.32Zm0 116.68-16 16a8 8 0 0 0 11.32 11.32l16-16a8 8 0 0 0-11.32-11.32ZM192 72a8 8 0 0 0 5.66-2.34l16-16a8 8 0 0 0-11.32-11.32l-16 16A8 8 0 0 0 192 72Zm5.66 114.34a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32-11.32ZM48 128a8 8 0 0 0-8-8H16a8 8 0 0 0 0 16h24a8 8 0 0 0 8-8Zm80 80a8 8 0 0 0-8 8v24a8 8 0 0 0 16 0v-24a8 8 0 0 0-8-8Zm112-88h-24a8 8 0 0 0 0 16h24a8 8 0 0 0 0-16Z" />
</svg>
)
}
function IconCopy({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8Zm-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z" />
</svg>
)
}
function IconCheck({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="m229.66 77.66-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69 218.34 66.34a8 8 0 0 1 11.32 11.32Z" />
</svg>
)
}
function IconDownload({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="M224 152v56a16 16 0 0 1-16 16H48a16 16 0 0 1-16-16v-56a8 8 0 0 1 16 0v56h160v-56a8 8 0 0 1 16 0Zm-101.66 5.66a8 8 0 0 0 11.32 0l40-40a8 8 0 0 0-11.32-11.32L136 132.69V40a8 8 0 0 0-16 0v92.69l-26.34-26.35a8 8 0 0 0-11.32 11.32Z" />
</svg>
)
}
function IconClose({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="M205.66 194.34a8 8 0 0 1-11.32 11.32L128 139.31l-66.34 66.35a8 8 0 0 1-11.32-11.32L116.69 128 50.34 61.66a8 8 0 0 1 11.32-11.32L128 116.69l66.34-66.35a8 8 0 0 1 11.32 11.32L139.31 128Z" />
</svg>
)
}
function IconEdit({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
/>
</svg>
)
}
function IconShare({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
className={cn('h-4 w-4', className)}
viewBox="0 0 256 256"
{...props}
>
<path d="m237.66 106.35-80-80A8 8 0 0 0 144 32v40.35c-25.94 2.22-54.59 14.92-78.16 34.91-28.38 24.08-46.05 55.11-49.76 87.37a12 12 0 0 0 20.68 9.58c11-11.71 50.14-48.74 107.24-52V192a8 8 0 0 0 13.66 5.65l80-80a8 8 0 0 0 0-11.3ZM160 172.69V144a8 8 0 0 0-8-8c-28.08 0-55.43 7.33-81.29 21.8a196.17 196.17 0 0 0-36.57 26.52c5.8-23.84 20.42-46.51 42.05-64.86C99.41 99.77 127.75 88 152 88a8 8 0 0 0 8-8V51.32L220.69 112Z" />
</svg>
)
}
function IconUsers({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
className={cn('h-4 w-4', className)}
viewBox="0 0 256 256"
{...props}
>
<path d="M117.25 157.92a60 60 0 1 0-66.5 0 95.83 95.83 0 0 0-47.22 37.71 8 8 0 1 0 13.4 8.74 80 80 0 0 1 134.14 0 8 8 0 0 0 13.4-8.74 95.83 95.83 0 0 0-47.22-37.71ZM40 108a44 44 0 1 1 44 44 44.05 44.05 0 0 1-44-44Zm210.14 98.7a8 8 0 0 1-11.07-2.33A79.83 79.83 0 0 0 172 168a8 8 0 0 1 0-16 44 44 0 1 0-16.34-84.87 8 8 0 1 1-5.94-14.85 60 60 0 0 1 55.53 105.64 95.83 95.83 0 0 1 47.22 37.71 8 8 0 0 1-2.33 11.07Z" />
</svg>
)
}
function IconExternalLink({
className,
...props
}: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
className={cn('h-4 w-4', className)}
viewBox="0 0 256 256"
{...props}
>
<path d="M224 104a8 8 0 0 1-16 0V59.32l-66.33 66.34a8 8 0 0 1-11.32-11.32L196.68 48H152a8 8 0 0 1 0-16h64a8 8 0 0 1 8 8Zm-40 24a8 8 0 0 0-8 8v72H48V80h72a8 8 0 0 0 0-16H48a16 16 0 0 0-16 16v128a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-72a8 8 0 0 0-8-8Z" />
</svg>
)
}
function IconChevronUpDown({
className,
...props
}: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
className={cn('h-4 w-4', className)}
viewBox="0 0 256 256"
{...props}
>
<path d="M181.66 170.34a8 8 0 0 1 0 11.32l-48 48a8 8 0 0 1-11.32 0l-48-48a8 8 0 0 1 11.32-11.32L128 212.69l42.34-42.35a8 8 0 0 1 11.32 0Zm-96-84.68L128 43.31l42.34 42.35a8 8 0 0 0 11.32-11.32l-48-48a8 8 0 0 0-11.32 0l-48 48a8 8 0 0 0 11.32 11.32Z" />
</svg>
)
}
export {
IconEdit,
IconNextChat,
IconOpenAI,
IconVercel,
IconGitHub,
IconSeparator,
IconArrowDown,
IconArrowRight,
IconUser,
IconPlus,
IconArrowElbow,
IconSpinner,
IconMessage,
IconTrash,
IconRefresh,
IconStop,
IconSidebar,
IconMoon,
IconSun,
IconCopy,
IconCheck,
IconDownload,
IconClose,
IconShare,
IconUsers,
IconExternalLink,
IconChevronUpDown
}

View File

@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,128 @@
import * as React from "react"
import { ChevronDownIcon } from "@radix-ui/react-icons"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props}
/>
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDownIcon
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className
)}
{...props}
/>
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}
{...props}
/>
</div>
))
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}

View File

@ -0,0 +1,121 @@
import * as React from "react"
import {
ChevronLeftIcon,
ChevronRightIcon,
DotsHorizontalIcon,
} from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
import { ButtonProps, buttonVariants } from "@/components/ui/button"
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
Pagination.displayName = "Pagination"
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
))
PaginationContent.displayName = "PaginationContent"
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
))
PaginationItem.displayName = "PaginationItem"
type PaginationLinkProps = {
isActive?: boolean
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">
const PaginationLink = ({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeftIcon className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
)
PaginationPrevious.displayName = "PaginationPrevious"
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>Next</span>
<ChevronRightIcon className="h-4 w-4" />
</PaginationLink>
)
PaginationNext.displayName = "PaginationNext"
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
)
PaginationEllipsis.displayName = "PaginationEllipsis"
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}

View File

@ -0,0 +1,33 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

View File

@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@ -0,0 +1,31 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }

View File

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

23
src/const/api.ts Normal file
View File

@ -0,0 +1,23 @@
const ExternalURL = ""
export const AttestationPath = ExternalURL + "/v1/webauthn/attestation";
export const DiscoverableAttestationPath = AttestationPath + "?discoverable=true";
export const AssertionPath = ExternalURL + "/v1/webauthn/assertion";
export const DiscoverableAssertionPath = AssertionPath + "?discoverable=true";
export const LoginPath = ExternalURL + "/api/login";
export const LogoutPath = ExternalURL + "/api/logout";
export const InfoPath = ExternalURL + "/api/info";
export const WebauthNUserNameHeader = "x-vaala-webauthn-username"
export const APIPrefix = "/api/v1"
export const FunctionPrefix = APIPrefix + "/f"
export const BizPrefix = APIPrefix + "/biz"
export const AIAPIPrefix = FunctionPrefix + "/ai.vaa.la"
export const SearchShareAPIPrefix = FunctionPrefix + "/cache.vaa.la"
export const ChatShareAPIPrefix = FunctionPrefix + "/chatshare.vaa.la"
export const HotNewsPrefix = BizPrefix + "/hotnews"

14
src/const/layout.ts Normal file
View File

@ -0,0 +1,14 @@
export const ToastGlobalConfig: {
position: 'top-right' | 'top-left' | 'top-center',
dismissible: boolean,
cancel?: {
label: string;
onClick?: () => void;
},
} = {
position: 'top-center',
dismissible: true,
cancel: {
label: '关闭',
}
}

View File

@ -0,0 +1 @@
export const ChatMessagesKey = "chat-messages-0"

View File

@ -0,0 +1,23 @@
import * as React from 'react'
export function useAtBottom(offset = 0) {
const [isAtBottom, setIsAtBottom] = React.useState(false)
React.useEffect(() => {
const handleScroll = () => {
setIsAtBottom(
window.innerHeight + window.scrollY >=
document.body.offsetHeight - offset
)
}
window.addEventListener('scroll', handleScroll, { passive: true })
handleScroll()
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [offset])
return isAtBottom
}

View File

@ -0,0 +1,33 @@
'use client'
import * as React from 'react'
export interface useCopyToClipboardProps {
timeout?: number
}
export function useCopyToClipboard({
timeout = 2000
}: useCopyToClipboardProps) {
const [isCopied, setIsCopied] = React.useState<Boolean>(false)
const copyToClipboard = (value: string) => {
if (typeof window === 'undefined' || !navigator.clipboard?.writeText) {
return
}
if (!value) {
return
}
navigator.clipboard.writeText(value).then(() => {
setIsCopied(true)
setTimeout(() => {
setIsCopied(false)
}, timeout)
})
}
return { isCopied, copyToClipboard }
}

View File

@ -0,0 +1,23 @@
import { useRef, type RefObject } from 'react'
export function useEnterSubmit(): {
formRef: RefObject<HTMLFormElement>
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void
} {
const formRef = useRef<HTMLFormElement>(null)
const handleKeyDown = (
event: React.KeyboardEvent<HTMLTextAreaElement>
): void => {
if (
event.key === 'Enter' &&
!event.shiftKey &&
!event.nativeEvent.isComposing
) {
formRef.current?.requestSubmit()
event.preventDefault()
}
}
return { formRef, onKeyDown: handleKeyDown }
}

View File

@ -0,0 +1,57 @@
"use client";
import { useEffect, useState } from 'react'
export const useLocalStorageString = (
key: string,
initialValue: string
): [string, (value: string) => void] => {
const [storedValue, setStoredValue] = useState(initialValue)
useEffect(() => {
// Retrieve from localStorage
if (typeof window !== 'undefined' && window.localStorage) {
const item = window.localStorage.getItem(key)
if (item) {
setStoredValue(item)
}
}
}, [key])
const setValue = (value: string) => {
if (typeof window !== 'undefined' && window.localStorage) {
// Save state
setStoredValue(value)
// Save to localStorage
window.localStorage.setItem(key, value)
}
}
return [storedValue, setValue]
}
export const useLocalStorage = <T>(
key: string,
initialValue: T
): [T, (value: T) => void] => {
const [storedValue, setStoredValue] = useState(initialValue)
useEffect(() => {
// Retrieve from localStorage
if (typeof window !== 'undefined' && window.localStorage) {
const item = window.localStorage.getItem(key)
if (item) {
setStoredValue(JSON.parse(item))
}
}
}, [key])
const setValue = (value: T) => {
// Save state
if (typeof window !== 'undefined' && window.localStorage) {
setStoredValue(value)
// Save to localStorage
window.localStorage.setItem(key, JSON.stringify(value))
}
}
return [storedValue, setValue]
}

6
src/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

5
src/store/chat.ts Normal file
View File

@ -0,0 +1,5 @@
import { atom } from 'nanostores'
import { type Message } from 'ai/react'
export const $messages = atom<Message[]>([])
export const $chatID = atom<string | undefined>()

3
src/store/user.ts Normal file
View File

@ -0,0 +1,3 @@
import { atom } from 'nanostores'
export const $statusOnline = atom<boolean>(false)

15
src/types/http.ts Normal file
View File

@ -0,0 +1,15 @@
export interface GetJWTResponse {
token: string;
}
export interface APIResponse<T> {
code: number;
msg: string;
body: T;
}
export interface OptDataAPIResponse<T> {
code: number;
msg: string;
body?: T;
}

3
src/types/relate.ts Normal file
View File

@ -0,0 +1,3 @@
export interface Relate {
question: string;
}

19
src/types/source.ts Normal file
View File

@ -0,0 +1,19 @@
export interface Source {
id: string;
name: string;
url: string;
isFamilyFriendly: boolean;
displayUrl: string;
snippet: string;
deepLinks: { snippet: string; name: string; url: string }[];
dateLastCrawled: string;
cachedPageUrl: string;
language: string;
primaryImageOfPage?: {
thumbnailUrl: string;
width: number;
height: number;
imageId: string;
};
isNavigational: boolean;
}

217
src/utils/base64.ts Normal file
View File

@ -0,0 +1,217 @@
/*
This file is a work taken from the following location: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
It has been modified using the below algorithms to make it work for the standard web encoding.
MIT License
Copyright (c) 2020 Egor Nepomnyaschih
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/*
// This constant can also be computed with the following algorithm:
const base64Chars = [],
A = "A".charCodeAt(0),
a = "a".charCodeAt(0),
n = "0".charCodeAt(0);
for (let i = 0; i < 26; ++i) {
base64Chars.push(String.fromCharCode(A + i));
}
for (let i = 0; i < 26; ++i) {
base64Chars.push(String.fromCharCode(a + i));
}
for (let i = 0; i < 10; ++i) {
base64Chars.push(String.fromCharCode(n + i));
}
base64Chars.push("+");
base64Chars.push("/");
*/
const base64Chars = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"+",
"/",
];
/*
// This constant can also be computed with the following algorithm:
const l = 256, base64codes = new Uint8Array(l);
for (let i = 0; i < l; ++i) {
base64codes[i] = 255; // invalid character
}
base64Chars.forEach((char, index) => {
base64codes[char.charCodeAt(0)] = index;
});
base64codes["=".charCodeAt(0)] = 0; // ignored anyway, so we just need to prevent an error
*/
const base64Codes = [
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 62, 255, 255, 255, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, 255, 0, 255, 255,
255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255,
255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
];
function getBase64Code(charCode: number) {
if (charCode >= base64Codes.length) {
throw new Error("Unable to parse base64 string.");
}
const code = base64Codes[charCode];
if (code === 255) {
throw new Error("Unable to parse base64 string.");
}
return code;
}
export function getBase64FromBytes(bytes: number[] | Uint8Array): string {
let result = "",
i,
l = bytes.length;
for (i = 2; i < l; i += 3) {
result += base64Chars[bytes[i - 2] >> 2];
result += base64Chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
result += base64Chars[((bytes[i - 1] & 0x0F) << 2) | (bytes[i] >> 6)];
result += base64Chars[bytes[i] & 0x3F];
}
if (i === l + 1) { // 1 octet yet to write
result += base64Chars[bytes[i - 2] >> 2];
result += base64Chars[(bytes[i - 2] & 0x03) << 4];
result += "==";
}
if (i === l) { // 2 octets yet to write
result += base64Chars[bytes[i - 2] >> 2];
result += base64Chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
result += base64Chars[(bytes[i - 1] & 0x0F) << 2];
result += "=";
}
return result;
}
export function getBase64WebEncodingFromBytes(bytes: number[] | Uint8Array): string {
return getBase64FromBytes(bytes)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
export function getBytesFromBase64(str: string): Uint8Array {
str = str.replace(/\-/g, "+")
.replace(/\_/g, "/");
// if (str.length % 4 !== 0) {
// throw new Error("Unable to parse base64 string.");
// }
const index = str.indexOf("=");
if (index !== -1 && index < str.length - 2) {
throw new Error("Unable to parse base64 string.");
}
let missingOctets = str.endsWith("==") ? 2 : str.endsWith("=") ? 1 : 0,
n = str.length,
result = new Uint8Array(3 * (n / 4)),
buffer;
for (let i = 0, j = 0; i < n; i += 4, j += 3) {
buffer =
getBase64Code(str.charCodeAt(i)) << 18 |
getBase64Code(str.charCodeAt(i + 1)) << 12 |
getBase64Code(str.charCodeAt(i + 2)) << 6 |
getBase64Code(str.charCodeAt(i + 3));
result[j] = buffer >> 16;
result[j + 1] = (buffer >> 8) & 0xFF;
result[j + 2] = buffer & 0xFF;
}
return result.subarray(0, result.length - missingOctets);
}

26
src/utils/fetch-stream.ts Normal file
View File

@ -0,0 +1,26 @@
async function pump(
reader: ReadableStreamDefaultReader<Uint8Array>,
controller: ReadableStreamDefaultController,
onChunk?: (chunk: Uint8Array) => void,
onDone?: () => void,
): Promise<ReadableStreamReadResult<Uint8Array> | undefined> {
const { done, value } = await reader.read();
if (done) {
onDone && onDone();
controller.close();
return;
}
onChunk && onChunk(value);
controller.enqueue(value);
return pump(reader, controller, onChunk, onDone);
}
export const fetchStream = (
response: Response,
onChunk?: (chunk: Uint8Array) => void,
onDone?: () => void,
): ReadableStream<string> => {
const reader = response.body!.getReader();
return new ReadableStream({
start: (controller) => pump(reader, controller, onChunk, onDone),
});
};

View File

@ -0,0 +1,6 @@
export const getSearchUrl = (query: string, search_uuid: string) => {
// const prefix =
// process.env.NODE_ENV === "production" ? "/search.html" : "/search";
const prefix = "/search";
return `${prefix}?q=${encodeURIComponent(query)}&rid=${search_uuid}`;
};

View File

@ -0,0 +1,99 @@
import { AIAPIPrefix, SearchShareAPIPrefix } from "@/const/api";
import { Relate } from "@/types/relate";
import { Source } from "@/types/source";
import { fetchStream } from "@/utils/fetch-stream";
const LLM_SPLIT = "__LLM_RESPONSE__";
const RELATED_SPLIT = "__RELATED_QUESTIONS__";
export const parseStreaming = async (
controller: AbortController,
query: string,
search_uuid: string,
mode: "query" | "share",
onSources: (value: Source[]) => void,
onMarkdown: (value: string) => void,
onRelates: (value: Relate[]) => void,
onServerQuery: (value: string) => void,
onError?: (status: number) => void,
) => {
const decoder = new TextDecoder();
let uint8Array = new Uint8Array();
let chunks = "";
let sourcesEmitted = false;
const response = await fetch(mode === "query" ? `${AIAPIPrefix}/v1/chat/search` : `${SearchShareAPIPrefix}/v1/chat/share`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "*./*",
},
signal: controller.signal,
body: JSON.stringify({
query,
search_uuid,
}),
});
if (response.status !== 200) {
onError?.(response.status);
return;
} else {
onError?.(200);
}
const markdownParse = (text: string) => {
onMarkdown(
text
.replace(/\[\[([cC])itation/g, "[citation")
.replace(/[cC]itation:(\d+)]]/g, "citation:$1]")
.replace(/\[\[([cC]itation:\d+)]](?!])/g, `[$1]`)
.replace(/\[[cC]itation:(\d+)]/g, "[citation]($1)")
.replace(/\[(\d+)\]/g, "[citation]($1)")
.replace(/\[引用:(\d+)\]/g, "[citation]($1)")
.replace(/\[引用(\d+)\]/g, "[citation]($1)"),
);
};
fetchStream(
response,
(chunk) => {
uint8Array = new Uint8Array([...uint8Array, ...chunk]);
chunks = decoder.decode(uint8Array, { stream: true });
if (chunks.includes(LLM_SPLIT)) {
const [sources, rest] = chunks.split(LLM_SPLIT);
if (!sourcesEmitted) {
try {
onServerQuery((JSON.parse(sources)).query);
onSources((JSON.parse(sources)).contexts);
onError?.(200);
} catch (e) {
try {
const shortsource = sources.slice(1)
onServerQuery((JSON.parse(shortsource)).query);
onSources((JSON.parse(shortsource)).contexts);
onError?.(200);
} catch (e) {
console.error("parse sources error", e);
}
}
}
sourcesEmitted = true;
if (rest.includes(RELATED_SPLIT)) {
const [md] = rest.split(RELATED_SPLIT);
markdownParse(md);
onError?.(200);
} else {
markdownParse(rest);
onError?.(200);
}
}
},
() => {
const [_, relates] = chunks.split(RELATED_SPLIT);
try {
onRelates(JSON.parse(relates));
onError?.(200);
window.history.replaceState(null, "", "/search?rid=" + search_uuid);
} catch (e) {
console.error("parse relates error", e);
}
},
);
};

75
storage/db.go Normal file
View File

@ -0,0 +1,75 @@
package storage
import (
"gorm.io/gorm"
)
const (
DefaultDBName = "default"
)
type DBManager interface {
GetDB(dbType, dbName string) *gorm.DB
GetDefaultDB() *gorm.DB
SetDB(dbType, dbName string, db *gorm.DB)
RemoveDB(dbType, dbName string)
Init(tables ...interface{})
}
type dbManagerImpl struct {
DBs map[string]map[string]*gorm.DB // [key: dbname, value: dbs] => [key: dbtype value: db]
defaultDBType string
}
func (dbm *dbManagerImpl) Init(tables ...interface{}) {
dbs := dbm.DBs[DefaultDBName]
for _, db := range dbs {
db.AutoMigrate(tables...)
}
}
var (
dbm *dbManagerImpl
)
func MustInitDBManager(dbs map[string]map[string]*gorm.DB, defaultDBType string) {
if dbm == nil {
dbm = NewDBManager(dbs, defaultDBType)
}
}
func NewDBManager(dbs map[string]map[string]*gorm.DB, defaultDBType string) *dbManagerImpl {
if dbs == nil {
dbs = make(map[string]map[string]*gorm.DB)
}
return &dbManagerImpl{
DBs: dbs,
defaultDBType: defaultDBType,
}
}
func GetDBManager() DBManager {
if dbm == nil {
dbm = NewDBManager(nil, "")
}
return dbm
}
func (dbm *dbManagerImpl) GetDB(dbType, dbName string) *gorm.DB {
return dbm.DBs[dbName][dbType]
}
func (dbm *dbManagerImpl) SetDB(dbType, dbName string, db *gorm.DB) {
if dbm.DBs[dbName] == nil {
dbm.DBs[dbName] = make(map[string]*gorm.DB)
}
dbm.DBs[dbName][dbType] = db
}
func (dbm *dbManagerImpl) RemoveDB(dbType, dbName string) {
delete(dbm.DBs[dbName], dbType)
}
func (dbm *dbManagerImpl) GetDefaultDB() *gorm.DB {
return dbm.DBs[DefaultDBName][dbm.defaultDBType]
}

80
tailwind.config.ts Normal file
View File

@ -0,0 +1,80 @@
import type { Config } from "tailwindcss"
const config = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config
export default config

41
tsconfig.json Normal file
View File

@ -0,0 +1,41 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"target": "es2016",
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"cmd/out/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

15
utils/base64.go Normal file
View File

@ -0,0 +1,15 @@
package utils
import "encoding/base64"
func Base64Encode(s string) string {
return base64.StdEncoding.EncodeToString([]byte(s))
}
func Base64Decode(s string) string {
data, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return ""
}
return string(data)
}

13
utils/crypto.go Normal file
View File

@ -0,0 +1,13 @@
package utils
import (
"crypto/hmac"
"crypto/sha256"
)
func ValidMAC(message, messageMAC, key []byte) bool {
mac := hmac.New(sha256.New, key)
mac.Write(message)
expectedMAC := mac.Sum(nil)
return hmac.Equal(messageMAC, expectedMAC)
}

21
utils/idgen.go Normal file
View File

@ -0,0 +1,21 @@
package utils
import (
"strings"
"github.com/google/uuid"
)
func GenerateUID() string {
return uuid.New().String()
}
func L32UID() string {
o := GenerateUID()
n := strings.Replace(o, "-", "", -1)
return n
}
func GenCode() string {
return strings.ToUpper(L32UID()[0:6])
}

65
utils/jwt.go Normal file
View File

@ -0,0 +1,65 @@
package utils
import (
"errors"
"github.com/golang-jwt/jwt/v5"
)
// @secretKey: JWT 加解密密钥
// @iat: 时间戳
// @seconds: 过期时间,单位秒
// @payload: 数据载体
func GetJwtToken(secretKey string, iat, seconds int64, payload string) (string, error) {
claims := make(jwt.MapClaims)
claims["exp"] = iat + seconds
claims["iat"] = iat
claims["payload"] = payload
token := jwt.New(jwt.SigningMethodHS256)
token.Claims = claims
return token.SignedString([]byte(secretKey))
}
// @secretKey: JWT 加解密密钥
// @iat: 时间戳
// @seconds: 过期时间,单位秒
// @payload: 数据载体
func GetJwtTokenFromMap(secretKey string, iat, seconds int64, payload map[string]string) (string, error) {
claims := make(jwt.MapClaims)
claims["exp"] = iat + seconds
claims["iat"] = iat
for k, v := range payload {
claims[k] = v
}
token := jwt.New(jwt.SigningMethodHS256)
token.Claims = claims
return token.SignedString([]byte(secretKey))
}
// @secretKey: JWT 加解密密钥
// @token: JWT Token 的字符串
func ValidateJwtToken(secretKey, token string) (bool, error) {
t, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
return []byte(secretKey), nil
})
if err != nil {
return false, err
}
return t.Valid, nil
}
func ParseToken(secretKey, tokenStr string) (u jwt.MapClaims, err error) {
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
return []byte(secretKey), nil
})
if err != nil {
return nil, errors.New("couldn't handle this token")
}
if t, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return t, nil
}
return nil, errors.New("couldn't handle this token")
}

51
watcher/corn_service.go Normal file
View File

@ -0,0 +1,51 @@
package watcher
import (
"context"
"github.com/nose7en/ToyBoomServer/common"
"github.com/go-co-op/gocron/v2"
)
type Client interface {
Run()
Stop()
}
type client struct {
s gocron.Scheduler
}
func NewClient(f func() error) Client {
c := context.Background()
s, err := gocron.NewScheduler()
if err != nil {
common.Logger(c).WithError(err).Fatalf("create scheduler error")
}
_, err = s.NewJob(
gocron.CronJob("*/30 * * * * *", true),
gocron.NewTask(f),
)
f()
if err != nil {
common.Logger(c).WithError(err).Fatalf("create job error")
}
return &client{
s: s,
}
}
func (c *client) Run() {
ctx := context.Background()
common.Logger(ctx).Infof("start to run scheduler, interval: 30s")
c.s.Start()
}
func (c *client) Stop() {
ctx := context.Background()
if err := c.s.Shutdown(); err != nil {
common.Logger(ctx).WithError(err).Errorf("shutdown scheduler error")
}
}