├── .github ├── dependabot.yml └── workflows │ ├── docker.yml │ └── go.yml ├── .gitignore ├── .golangci.yml ├── Dockerfile ├── LICENSE ├── README.md ├── README_zh.md ├── elaina.gif ├── elaina.go ├── embed ├── elaina.html └── elaina.js ├── go.mod ├── go.sum ├── internal ├── config │ └── config.go ├── context │ ├── context.go │ └── session.go ├── db │ ├── database.go │ ├── sandboxes.go │ └── templates.go ├── dbutil │ ├── db.go │ ├── model.go │ └── pagination.go ├── form │ ├── auth.go │ ├── form.go │ ├── sandbox.go │ └── template.go ├── languages │ ├── languages.go │ └── runner.go ├── ratelimit │ └── ratelimit.go ├── route │ ├── auth.go │ ├── frontend.go │ ├── route.go │ ├── runner.go │ ├── sandbox.go │ └── template.go └── runtime │ ├── docker.go │ ├── kubernetes.go │ └── runtime.go ├── public ├── css │ └── sandbox.css ├── fs.go └── js │ └── sandbox.js ├── templates ├── fs.go └── sandbox.tmpl └── web ├── .gitignore ├── fs.go ├── index.html ├── package.json ├── pnpm-lock.yaml ├── src ├── App.vue ├── api │ ├── auth.ts │ ├── interceptor.ts │ ├── sandbox.ts │ └── template.ts ├── components │ └── Header.vue ├── const │ └── template.ts ├── main.ts ├── route │ └── index.ts ├── store │ ├── index.ts │ └── modules │ │ └── auth │ │ ├── index.ts │ │ └── types.ts ├── style.css ├── style │ ├── font-family.less │ ├── form.less │ ├── index.less │ ├── reset.less │ └── variables.less ├── theme.css ├── views │ ├── Dashboard.vue │ ├── Layout.vue │ ├── Sandbox.vue │ ├── SandboxModify.vue │ ├── SignIn.vue │ ├── Template.vue │ └── TemplateModify.vue └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | on: 3 | push: 4 | branches: [ master ] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Compute image tag name 11 | run: echo "IMAGE_TAG=$(date -u '+%Y%m%d-%H%M%S')" >> $GITHUB_ENV 12 | 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Login to Dockerhub 17 | uses: docker/login-action@v2 18 | with: 19 | registry: registry.hub.docker.com 20 | username: ${{ secrets.DOCKER_USERNAME }} 21 | password: ${{ secrets.DOCKER_PASSWORD }} 22 | 23 | - name: Build image 24 | uses: docker/build-push-action@v4 25 | with: 26 | context: . 27 | platforms: linux/amd64 28 | push: true 29 | build-args: | 30 | GITHUB_SHA=${{ github.sha }} 31 | tags: | 32 | registry.hub.docker.com/wuhan005/elaina:${{ env.IMAGE_TAG }} 33 | registry.hub.docker.com/wuhan005/elaina:latest 34 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | push: 4 | branches: [ master ] 5 | paths: 6 | - '**.go' 7 | - 'go.mod' 8 | - '.github/workflows/go.yml' 9 | pull_request: 10 | paths: 11 | - '**.go' 12 | - 'go.mod' 13 | - '.github/workflows/go.yml' 14 | env: 15 | GOPROXY: "https://proxy.golang.org" 16 | 17 | jobs: 18 | lint: 19 | name: Lint 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v2 24 | - name: Create frontend dist folder 25 | run: mkdir web/dist/ && touch web/dist/1 26 | - name: Install Go 27 | uses: actions/setup-go@v5 28 | with: 29 | go-version: 1.22.x 30 | - name: Run golangci-lint 31 | uses: golangci/golangci-lint-action@v4 32 | with: 33 | version: v1.54 34 | args: --timeout=10m 35 | - name: Check Go module tidiness 36 | shell: bash 37 | run: | 38 | go mod tidy 39 | STATUS=$(git status --porcelain) 40 | if [ ! -z "$STATUS" ]; then 41 | echo "Unstaged files:" 42 | echo $STATUS 43 | echo "Run 'go mod tidy' commit them" 44 | exit 1 45 | fi 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | .idea/ 18 | .bin/ 19 | .task 20 | .envrc 21 | .elaina 22 | volume/ 23 | Elaina 24 | /.DS_Store 25 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | nakedret: 3 | max-func-lines: 0 4 | govet: 5 | settings: 6 | printf: 7 | funcs: 8 | - (unknwon.dev/clog/v2).Trace 9 | - (unknwon.dev/clog/v2).Info 10 | - (unknwon.dev/clog/v2).Warn 11 | - (unknwon.dev/clog/v2).Error 12 | - (unknwon.dev/clog/v2).ErrorDepth 13 | - (unknwon.dev/clog/v2).Fatal 14 | - (unknwon.dev/clog/v2).FatalDepth 15 | 16 | linters: 17 | enable: 18 | - deadcode 19 | - errcheck 20 | - gosimple 21 | - govet 22 | - ineffassign 23 | - staticcheck 24 | - structcheck 25 | - typecheck 26 | - unused 27 | - varcheck 28 | - nakedret 29 | - gofmt 30 | - rowserrcheck 31 | - unconvert 32 | 33 | run: 34 | skip-dirs: 35 | - frontend -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest as node_builder 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | WORKDIR /app/web 8 | RUN rm -rf node_modules || true 9 | RUN npm install -g pnpm 10 | RUN git init 11 | RUN pnpm install 12 | RUN pnpm run build 13 | 14 | FROM golang:1.22-alpine as go_builder 15 | 16 | WORKDIR /app 17 | 18 | ENV CGO_ENABLED=0 19 | 20 | COPY . . 21 | COPY --from=node_builder /app/web/dist ./web/dist 22 | 23 | RUN go mod tidy 24 | RUN go build -v -trimpath -ldflags "-w -s -extldflags '-static'" -o elaina . 25 | 26 | FROM alpine:latest 27 | 28 | RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* 29 | RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 30 | RUN echo 'Asia/Shanghai' > /etc/timezone 31 | 32 | RUN mkdir /etc/elaina 33 | WORKDIR /etc/elaina 34 | 35 | COPY --from=go_builder /app/elaina /etc/elaina/elaina 36 | 37 | RUN chmod 655 /etc/elaina/elaina 38 | 39 | ENTRYPOINT ["/etc/elaina/elaina"] 40 | EXPOSE 8080 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 E99p1ant 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elaina ![Go](https://github.com/wuhan005/Elaina/workflows/Go/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/wuhan005/Elaina)](https://goreportcard.com/report/github.com/wuhan005/Elaina) ![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/wuhan005/elaina) ![Docker Image Version (latest by date)](https://img.shields.io/docker/v/wuhan005/elaina) 2 | 3 | 4 | Container-based remote code runner. 5 | 6 | [简体中文](https://github.com/wuhan005/Elaina/blob/master/README_zh.md) 7 | 8 | ## Start 9 | 10 | ### Step 1: Install dependencies 11 | 12 | * [Docker](https://docs.docker.com/get-docker/) (v20.10.0 or higher) 13 | * [Postgres](https://www.postgresql.org/download/) (v13.1 or higher) 14 | 15 | ### Step 2: Pull internal docker images 16 | 17 | Use `docker pull` command to pull the images from DockerHub before you start running the Elaina. This operation only 18 | needs to be performed once. 19 | 20 | ```bash 21 | docker pull glot/php:latest 22 | docker pull glot/python:latest 23 | docker pull glot/golang:latest 24 | docker pull glot/javascript:latest 25 | docker pull glot/c:latest 26 | ``` 27 | 28 | ### Step 3: Build and start the Elaina server 29 | 30 | #### Build Elaina 31 | 32 | ```bash 33 | git clone git@github.com:wuhan005/Elaina.git 34 | 35 | # Build frontend 36 | cd frontend/ && yarn install && yarn build 37 | 38 | # Build backend 39 | go build . 40 | ``` 41 | 42 | #### Set environment variables. 43 | 44 | ```bash 45 | export APP_PASSWORD= 46 | export RUNTIME_MODE=docker 47 | export POSTGRES_DSN=postgres://postgres:@127.0.0.1:5432/elaina 48 | ``` 49 | 50 | #### Run the Elaina server. 51 | 52 | ```bash 53 | ./Elaina 54 | ``` 55 | 56 | ### Step 4: Have fun! 57 | 58 | Visit `http://:8080/` to login to the manager panel. 59 | 60 | ## License 61 | 62 | MIT License 63 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # Elaina ![Go](https://github.com/wuhan005/Elaina/workflows/Go/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/wuhan005/Elaina)](https://goreportcard.com/report/github.com/wuhan005/Elaina) ![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/wuhan005/elaina) ![Docker Image Version (latest by date)](https://img.shields.io/docker/v/wuhan005/elaina) 2 | 3 | 4 | 基于容器的远程代码运行器。 5 | 6 | ## 开始使用 7 | 8 | ### 步骤 1: 安装依赖 9 | 10 | * [Docker](https://docs.docker.com/get-docker/) (v20.10.0 或更高) 11 | * [Postgres](https://www.postgresql.org/download/) (v13.1 或更高) 12 | 13 | ### 步骤 2: 拉取内置 Docker 镜像 14 | 15 | 在运行 Elaina 前,请使用 `docker pull` 命令从 DockerHub 拉取这些镜像。该操作只需执行一次即可。 16 | 17 | ```bash 18 | docker pull glot/php:latest 19 | docker pull glot/python:latest 20 | docker pull glot/golang:latest 21 | docker pull glot/javascript:latest 22 | docker pull glot/c:latest 23 | ``` 24 | 25 | ### Step 3: 编译并启动 Elaina 26 | 27 | #### 编译 Elaina 28 | 29 | ```bash 30 | git clone git@github.com:wuhan005/Elaina.git 31 | 32 | # 编译前端 33 | cd frontend/ && yarn install && yarn build 34 | 35 | # 编译后端 36 | go build . 37 | ``` 38 | 39 | #### 设置环境变量 40 | 41 | ```bash 42 | export APP_PASSWORD= 43 | export RUNTIME_MODE=docker 44 | export POSTGRES_DSN=postgres://postgres:@127.0.0.1:5432/elaina 45 | ``` 46 | 47 | #### 运行 Elaina 48 | 49 | ```bash 50 | ./Elaina 51 | ``` 52 | 53 | ### 步骤 4: 走你! 54 | 55 | 浏览器访问 `http://:8080/` 来登录管理员面板。 56 | 57 | ## 开源协议 58 | 59 | MIT License 60 | -------------------------------------------------------------------------------- /elaina.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuhan005/Elaina/6feaff8515b1793767539df7034f24fda97f2dbc/elaina.gif -------------------------------------------------------------------------------- /elaina.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/sirupsen/logrus" 7 | 8 | "github.com/wuhan005/Elaina/internal/config" 9 | "github.com/wuhan005/Elaina/internal/db" 10 | "github.com/wuhan005/Elaina/internal/route" 11 | ) 12 | 13 | func main() { 14 | port := flag.Int("port", 8080, "Web service port") 15 | flag.Parse() 16 | 17 | if err := config.Init(); err != nil { 18 | logrus.WithError(err).Fatal("Failed to init config") 19 | } 20 | 21 | // Check environment config, make sure the application is safe enough. 22 | appPassword := config.App.Password 23 | if appPassword == "" || len(appPassword) < 8 { 24 | logrus.Fatal("APP_PASSWORD is not strong enough") 25 | } 26 | 27 | dbInstance, err := db.Init() 28 | if err != nil { 29 | logrus.WithError(err).Fatal("Failed to connect to database") 30 | } 31 | 32 | r, err := route.New(dbInstance) 33 | if err != nil { 34 | logrus.WithError(err).Fatal("Failed to create route") 35 | } 36 | r.Run(*port) 37 | } 38 | -------------------------------------------------------------------------------- /embed/elaina.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Elaina demo 4 | 5 | 6 | 7 |
8 | 9 | 10 | 24 | 25 | -------------------------------------------------------------------------------- /embed/elaina.js: -------------------------------------------------------------------------------- 1 | function Elaina(obj) { 2 | let host = obj.host.replace(/\/+$/, ''); 3 | let uid = obj.uid; 4 | let el = obj.el; 5 | let language = obj.language; 6 | let height = obj.height; 7 | 8 | let frame = document.createElement('iframe'); 9 | frame.src = host + '/r/' + uid; 10 | frame.width = '100%'; 11 | frame.height = height; 12 | frame.frameBorder = 0; 13 | frame.onload = () => { 14 | frame.contentWindow.postMessage({ 15 | type: 'elaina', 16 | language: language, 17 | }, host) 18 | } 19 | 20 | let elainaElement = document.getElementById(el); 21 | elainaElement.appendChild(frame); 22 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wuhan005/Elaina 2 | 3 | go 1.22.2 4 | 5 | require ( 6 | github.com/docker/docker v26.1.3+incompatible 7 | github.com/flamego/flamego v1.9.4 8 | github.com/flamego/session v1.6.5 9 | github.com/flamego/template v1.2.2 10 | github.com/kelseyhightower/envconfig v1.4.0 11 | github.com/lib/pq v1.10.9 12 | github.com/pkg/errors v0.9.1 13 | github.com/samber/lo v1.39.0 14 | github.com/satori/go.uuid v1.2.0 15 | github.com/sirupsen/logrus v1.9.3 16 | github.com/thanhpk/randstr v1.0.6 17 | github.com/wuhan005/gadget v0.0.0-20221206194113-7619e407f1a0 18 | github.com/wuhan005/govalid v0.0.1 19 | golang.org/x/text v0.15.0 20 | gorm.io/datatypes v1.2.0 21 | gorm.io/driver/postgres v1.5.7 22 | gorm.io/gorm v1.25.10 23 | k8s.io/api v0.30.1 24 | k8s.io/apimachinery v0.30.1 25 | k8s.io/client-go v0.30.1 26 | ) 27 | 28 | require ( 29 | filippo.io/edwards25519 v1.1.0 // indirect 30 | github.com/Microsoft/go-winio v0.6.2 // indirect 31 | github.com/alecthomas/participle/v2 v2.1.1 // indirect 32 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 33 | github.com/charmbracelet/lipgloss v0.11.0 // indirect 34 | github.com/charmbracelet/log v0.4.0 // indirect 35 | github.com/charmbracelet/x/ansi v0.1.1 // indirect 36 | github.com/containerd/log v0.1.0 // indirect 37 | github.com/davecgh/go-spew v1.1.1 // indirect 38 | github.com/distribution/reference v0.6.0 // indirect 39 | github.com/docker/go-connections v0.5.0 // indirect 40 | github.com/docker/go-units v0.5.0 // indirect 41 | github.com/emicklei/go-restful/v3 v3.12.0 // indirect 42 | github.com/felixge/httpsnoop v1.0.4 // indirect 43 | github.com/go-logfmt/logfmt v0.6.0 // indirect 44 | github.com/go-logr/logr v1.4.2 // indirect 45 | github.com/go-logr/stdr v1.2.2 // indirect 46 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 47 | github.com/go-openapi/jsonreference v0.21.0 // indirect 48 | github.com/go-openapi/swag v0.23.0 // indirect 49 | github.com/go-sql-driver/mysql v1.8.1 // indirect 50 | github.com/gogo/protobuf v1.3.2 // indirect 51 | github.com/golang/protobuf v1.5.4 // indirect 52 | github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect 53 | github.com/google/gofuzz v1.2.0 // indirect 54 | github.com/google/uuid v1.6.0 // indirect 55 | github.com/gorilla/websocket v1.5.1 // indirect 56 | github.com/jackc/pgpassfile v1.0.0 // indirect 57 | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect 58 | github.com/jackc/pgx/v5 v5.6.0 // indirect 59 | github.com/jackc/puddle/v2 v2.2.1 // indirect 60 | github.com/jinzhu/inflection v1.0.0 // indirect 61 | github.com/jinzhu/now v1.1.5 // indirect 62 | github.com/josharian/intern v1.0.0 // indirect 63 | github.com/json-iterator/go v1.1.12 // indirect 64 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 65 | github.com/mailru/easyjson v0.7.7 // indirect 66 | github.com/mattn/go-isatty v0.0.20 // indirect 67 | github.com/mattn/go-runewidth v0.0.15 // indirect 68 | github.com/mattn/go-sqlite3 v1.14.16 // indirect 69 | github.com/moby/docker-image-spec v1.3.1 // indirect 70 | github.com/moby/spdystream v0.2.0 // indirect 71 | github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect 72 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 73 | github.com/modern-go/reflect2 v1.0.2 // indirect 74 | github.com/morikuni/aec v1.0.0 // indirect 75 | github.com/muesli/termenv v0.15.2 // indirect 76 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 77 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 78 | github.com/opencontainers/go-digest v1.0.0 // indirect 79 | github.com/opencontainers/image-spec v1.1.0 // indirect 80 | github.com/rivo/uniseg v0.4.7 // indirect 81 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect 82 | go.opentelemetry.io/otel v1.27.0 // indirect 83 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect 84 | go.opentelemetry.io/otel/metric v1.27.0 // indirect 85 | go.opentelemetry.io/otel/sdk v1.19.0 // indirect 86 | go.opentelemetry.io/otel/trace v1.27.0 // indirect 87 | golang.org/x/crypto v0.23.0 // indirect 88 | golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect 89 | golang.org/x/net v0.25.0 // indirect 90 | golang.org/x/oauth2 v0.20.0 // indirect 91 | golang.org/x/sync v0.7.0 // indirect 92 | golang.org/x/sys v0.20.0 // indirect 93 | golang.org/x/term v0.20.0 // indirect 94 | golang.org/x/time v0.5.0 // indirect 95 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect 96 | google.golang.org/grpc v1.64.0 // indirect 97 | google.golang.org/protobuf v1.34.1 // indirect 98 | gopkg.in/inf.v0 v0.9.1 // indirect 99 | gopkg.in/yaml.v2 v2.4.0 // indirect 100 | gopkg.in/yaml.v3 v3.0.1 // indirect 101 | gorm.io/driver/mysql v1.5.6 // indirect 102 | gorm.io/driver/sqlite v1.4.4 // indirect 103 | gotest.tools/v3 v3.0.3 // indirect 104 | k8s.io/klog/v2 v2.120.1 // indirect 105 | k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a // indirect 106 | k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 // indirect 107 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 108 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 109 | sigs.k8s.io/yaml v1.4.0 // indirect 110 | ) 111 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= 4 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= 5 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 6 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 7 | github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= 8 | github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= 9 | github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= 10 | github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= 11 | github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= 12 | github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 13 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 14 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 15 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 16 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 17 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 18 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 19 | github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g= 20 | github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8= 21 | github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= 22 | github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= 23 | github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk= 24 | github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= 25 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 26 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 27 | github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 28 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 30 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 31 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 32 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 33 | github.com/docker/docker v26.1.3+incompatible h1:lLCzRbrVZrljpVNobJu1J2FHk8V0s4BawoZippkc+xo= 34 | github.com/docker/docker v26.1.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 35 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 36 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 37 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 38 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 39 | github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= 40 | github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 41 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 42 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 43 | github.com/flamego/flamego v1.9.4 h1:SNsooIfNa6ljQM1rBmfg4cFcXPIhQdG/uvNHqXxPvD8= 44 | github.com/flamego/flamego v1.9.4/go.mod h1:2tAVbugA3fgX8xOBoqR2jmJSSvZDLBFGXTFCR5h5eAU= 45 | github.com/flamego/session v1.6.5 h1:YlQfMGjV84JcGihg5OjufKP5qOh/05iOfHYrf5qta5I= 46 | github.com/flamego/session v1.6.5/go.mod h1:EhBKxrWSmkqa2XwQSC6WbwXn7pLzyFY0BREtTwJBpQU= 47 | github.com/flamego/template v1.2.2 h1:aMpt8RzXBb2ZGuABf9p/q8oBBpXrurUV8rgBbz7mj2o= 48 | github.com/flamego/template v1.2.2/go.mod h1:xTAmwCCPaOuxN5t4CpzOP7WZN5WkLRiJfJCpsiB0aUg= 49 | github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 50 | github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 51 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 52 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 53 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 54 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 55 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 56 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 57 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 58 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 59 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 60 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 61 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 62 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 63 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 64 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 65 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 66 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 67 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 68 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 69 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 70 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 71 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 72 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= 73 | github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= 74 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 75 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 76 | github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU= 77 | github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M= 78 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 79 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 80 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 81 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 82 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 83 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 84 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 85 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 86 | github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= 87 | github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= 88 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 89 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 90 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 91 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 92 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 93 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 94 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 95 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= 96 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= 97 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 98 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 99 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 100 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 101 | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= 102 | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 103 | github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= 104 | github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= 105 | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 106 | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 107 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 108 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 109 | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 110 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 111 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 112 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 113 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 114 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 115 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 116 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 117 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 118 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= 119 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 120 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 121 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 122 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 123 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 124 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 125 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 126 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 127 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 128 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 129 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 130 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 131 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 132 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 133 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 134 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 135 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 136 | github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 137 | github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= 138 | github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 139 | github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= 140 | github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= 141 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 142 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 143 | github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= 144 | github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= 145 | github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= 146 | github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= 147 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 148 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 149 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 150 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 151 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 152 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 153 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 154 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 155 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 156 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 157 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 158 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= 159 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 160 | github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g= 161 | github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= 162 | github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= 163 | github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= 164 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 165 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 166 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 167 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 168 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 169 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 170 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 171 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 172 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 173 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 174 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 175 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 176 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 177 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 178 | github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= 179 | github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= 180 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 181 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 182 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 183 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 184 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 185 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 186 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 187 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 188 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 189 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 190 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 191 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 192 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 193 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 194 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 195 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 196 | github.com/thanhpk/randstr v1.0.6 h1:psAOktJFD4vV9NEVb3qkhRSMvYh4ORRaj1+w/hn4B+o= 197 | github.com/thanhpk/randstr v1.0.6/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U= 198 | github.com/wuhan005/gadget v0.0.0-20221206194113-7619e407f1a0 h1:zOXiOJRG/FOohTliJiykpwIaCPtUTIh+G0jw2bOJkA8= 199 | github.com/wuhan005/gadget v0.0.0-20221206194113-7619e407f1a0/go.mod h1:vmC2IdgzTpIRwn1ZpuV/I3k9AIbRJ7oqTHFenq/qwkE= 200 | github.com/wuhan005/govalid v0.0.1 h1:ktJ2jNQ2gYNrWP5Gm+MDiydd2WpsdmUy5onUX/kXVwc= 201 | github.com/wuhan005/govalid v0.0.1/go.mod h1:3tGYqyWM99ZtcRH00WmDlvaHVCOAffYJbeUnj5YsTOU= 202 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 203 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 204 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 205 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= 206 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= 207 | go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= 208 | go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= 209 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= 210 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= 211 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= 212 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= 213 | go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= 214 | go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= 215 | go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= 216 | go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= 217 | go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= 218 | go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= 219 | go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= 220 | go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= 221 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 222 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 223 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 224 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 225 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= 226 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 227 | golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4= 228 | golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 229 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 230 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 231 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 232 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 233 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 234 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 235 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 236 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 237 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 238 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 239 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 240 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 241 | golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= 242 | golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 243 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 244 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 245 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 246 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 247 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 248 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 249 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 250 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 251 | golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 252 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 253 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 254 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 255 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 256 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 257 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 258 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 259 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 260 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 261 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 262 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 263 | golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= 264 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 265 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 266 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 267 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 268 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 269 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= 270 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 271 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 272 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 273 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 274 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 275 | golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 276 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 277 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 278 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 279 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 280 | golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= 281 | golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 282 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 283 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 284 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 285 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 286 | google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4= 287 | google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= 288 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e h1:Elxv5MwEkCI9f5SkoL6afed6NTdxaGoAo39eANBwHL8= 289 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= 290 | google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= 291 | google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= 292 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 293 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 294 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 295 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 296 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 297 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 298 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 299 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 300 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 301 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 302 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 303 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 304 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 305 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 306 | gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco= 307 | gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04= 308 | gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= 309 | gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= 310 | gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= 311 | gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= 312 | gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc= 313 | gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= 314 | gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= 315 | gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig= 316 | gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= 317 | gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 318 | gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= 319 | gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 320 | gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= 321 | gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= 322 | gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= 323 | k8s.io/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY= 324 | k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM= 325 | k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U= 326 | k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= 327 | k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q= 328 | k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc= 329 | k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= 330 | k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 331 | k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a h1:zD1uj3Jf+mD4zmA7W+goE5TxDkI7OGJjBNBzq5fJtLA= 332 | k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a/go.mod h1:UxDHUPsUwTOOxSU+oXURfFBcAS6JwiRXTYqYwfuGowc= 333 | k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak= 334 | k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 335 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 336 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 337 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 338 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 339 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 340 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 341 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package config 6 | 7 | import ( 8 | "github.com/kelseyhightower/envconfig" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | var App struct { 13 | Password string `envconfig:"APP_PASSWORD"` 14 | RuntimeMode string `envconfig:"RUNTIME_MODE" default:"docker"` 15 | 16 | KubernetesServiceHost string `envconfig:"KUBERNETES_SERVICE_HOST"` 17 | KubernetesNamespace string `envconfig:"KUBERNETES_NAMESPACE"` 18 | KubernetesCAData string `envconfig:"KUBERNETES_CA_DATA"` 19 | KubernetesCertData string `envconfig:"KUBERNETES_CERT_DATA"` 20 | KubernetesKeyData string `envconfig:"KUBERNETES_KEY_DATA"` 21 | KubernetesBearerToken string `envconfig:"KUBERNETES_BEARER_TOKEN"` 22 | } 23 | 24 | var Postgres struct { 25 | DSN string `envconfig:"POSTGRES_DSN"` 26 | } 27 | 28 | func Init() error { 29 | if err := envconfig.Process("", &App); err != nil { 30 | return errors.Wrap(err, "parse app") 31 | } 32 | if err := envconfig.Process("", &Postgres); err != nil { 33 | return errors.Wrap(err, "parse postgres") 34 | } 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/context/context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package context 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "net/http" 11 | 12 | "github.com/flamego/flamego" 13 | "github.com/flamego/session" 14 | "github.com/sirupsen/logrus" 15 | "gorm.io/gorm" 16 | 17 | "github.com/wuhan005/Elaina/internal/dbutil" 18 | ) 19 | 20 | // Context represents context of a request. 21 | type Context struct { 22 | flamego.Context 23 | IsAuthenticated bool 24 | } 25 | 26 | func (c *Context) Success(data ...interface{}) error { 27 | c.ResponseWriter().Header().Set("Content-Type", "application/json; charset=utf-8") 28 | c.ResponseWriter().WriteHeader(http.StatusOK) 29 | 30 | var d interface{} 31 | if len(data) == 1 { 32 | d = data[0] 33 | } 34 | 35 | err := json.NewEncoder(c.ResponseWriter()).Encode( 36 | map[string]interface{}{ 37 | "data": d, 38 | }, 39 | ) 40 | if err != nil { 41 | logrus.WithContext(c.Request().Context()).WithError(err).Error("Failed to encode") 42 | } 43 | return nil 44 | } 45 | 46 | func (c *Context) ServerError() error { 47 | return c.Error(http.StatusInternalServerError, "internal server error") 48 | } 49 | 50 | func (c *Context) Error(statusCode int, message string, v ...interface{}) error { 51 | c.ResponseWriter().Header().Set("Content-Type", "application/json; charset=utf-8") 52 | c.ResponseWriter().WriteHeader(statusCode) 53 | 54 | err := json.NewEncoder(c.ResponseWriter()).Encode( 55 | map[string]interface{}{ 56 | "msg": fmt.Sprintf(message, v...), 57 | }, 58 | ) 59 | if err != nil { 60 | logrus.WithContext(c.Request().Context()).WithError(err).Error("Failed to encode") 61 | } 62 | return nil 63 | } 64 | 65 | func (c *Context) Status(statusCode int) { 66 | c.ResponseWriter().WriteHeader(statusCode) 67 | } 68 | 69 | const ( 70 | SessionIDIsAuthenticated = "_isAuthenticated" 71 | ) 72 | 73 | // Contexter initializes a classic context for a request. 74 | func Contexter(gormDB *gorm.DB) flamego.Handler { 75 | return func(ctx flamego.Context, session session.Session) { 76 | c := Context{ 77 | Context: ctx, 78 | IsAuthenticated: false, 79 | } 80 | 81 | isAuthenticatedInf := session.Get(SessionIDIsAuthenticated) 82 | if isAuthenticated, ok := isAuthenticatedInf.(bool); ok && isAuthenticated { 83 | c.IsAuthenticated = true 84 | } 85 | 86 | c.MapTo(gormDB, (*dbutil.Transactor)(nil)) 87 | c.Map(c) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /internal/context/session.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package context 6 | 7 | import ( 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | func ReadIDFunc(r *http.Request) string { 13 | authorizationHeader := r.Header.Get("Authorization") 14 | groups := strings.SplitN(authorizationHeader, " ", 2) 15 | if len(groups) != 2 { 16 | return "" 17 | } 18 | return strings.TrimSpace(groups[1]) 19 | } 20 | 21 | func WriteIDFunc(w http.ResponseWriter, r *http.Request, sid string, created bool) { 22 | // Do nothing. 23 | } 24 | -------------------------------------------------------------------------------- /internal/db/database.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "gorm.io/driver/postgres" 6 | "gorm.io/gorm" 7 | 8 | "github.com/wuhan005/Elaina/internal/config" 9 | ) 10 | 11 | func Init() (*gorm.DB, error) { 12 | dsn := config.Postgres.DSN 13 | 14 | db, err := gorm.Open( 15 | postgres.Open(dsn), 16 | &gorm.Config{}, 17 | ) 18 | if err != nil { 19 | return nil, errors.Wrap(err, "open database") 20 | } 21 | 22 | if err := db.AutoMigrate(&Tpl{}, &Sandbox{}); err != nil { 23 | return nil, errors.Wrap(err, "auto migrate") 24 | } 25 | 26 | Tpls = NewTplStore(db) 27 | Sandboxes = NewSandboxStore(db) 28 | 29 | return db, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/db/sandboxes.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/thanhpk/randstr" 8 | "gorm.io/gorm" 9 | 10 | "github.com/wuhan005/Elaina/internal/dbutil" 11 | ) 12 | 13 | var _ SandboxStore = (*sandboxes)(nil) 14 | 15 | var Sandboxes SandboxStore 16 | 17 | type SandboxStore interface { 18 | // All returns all the sandboxes. 19 | All(ctx context.Context) ([]*Sandbox, error) 20 | // List returns the sandboxes with the given options. 21 | List(ctx context.Context, options ListSandboxOptions) ([]*Sandbox, int64, error) 22 | // GetByID returns a sandbox with the given id. 23 | GetByID(ctx context.Context, id uint) (*Sandbox, error) 24 | // GetByUID returns a sandbox with the given uid. 25 | GetByUID(ctx context.Context, uid string) (*Sandbox, error) 26 | // Create creates a new sandbox with the given options. 27 | Create(ctx context.Context, options CreateSandboxOptions) (*Sandbox, error) 28 | // Update edits a new sandbox with the given options. 29 | Update(ctx context.Context, id uint, options UpdateSandboxOptions) error 30 | // Delete deletes a sandbox with the given id. 31 | Delete(ctx context.Context, id uint) error 32 | } 33 | 34 | func NewSandboxStore(db *gorm.DB) SandboxStore { 35 | return &sandboxes{db} 36 | } 37 | 38 | type sandboxes struct { 39 | *gorm.DB 40 | } 41 | 42 | type Sandbox struct { 43 | dbutil.Model 44 | 45 | UID string `gorm:"NOT NULL" json:"uid"` 46 | Name string `json:"name"` 47 | TemplateID uint `gorm:"NOT NULL" json:"templateID"` 48 | Template *Tpl `gorm:"ForeignKey:TemplateID" json:"template"` 49 | Placeholder string `json:"placeholder"` 50 | Editable bool `json:"editable"` 51 | } 52 | 53 | var ErrSandboxNotFound = errors.New("sandbox dose not exist") 54 | 55 | func (db *sandboxes) getBy(ctx context.Context, query string, args ...interface{}) (*Sandbox, error) { 56 | var sandbox Sandbox 57 | if err := db.WithContext(ctx).Preload("Template").Model(&Sandbox{}).Where(query, args...).First(&sandbox).Error; err != nil { 58 | if errors.Is(err, gorm.ErrRecordNotFound) { 59 | return nil, ErrSandboxNotFound 60 | } 61 | return nil, errors.Wrap(err, "first") 62 | } 63 | return &sandbox, nil 64 | } 65 | 66 | func (db *sandboxes) GetByID(ctx context.Context, id uint) (*Sandbox, error) { 67 | return db.getBy(ctx, "id = ?", id) 68 | } 69 | 70 | func (db *sandboxes) GetByUID(ctx context.Context, uid string) (*Sandbox, error) { 71 | return db.getBy(ctx, "uid = ?", uid) 72 | } 73 | 74 | func (db *sandboxes) All(ctx context.Context) ([]*Sandbox, error) { 75 | var sandboxes []*Sandbox 76 | if err := db.WithContext(ctx).Preload("Template").Model(&Sandbox{}).Find(&sandboxes).Error; err != nil { 77 | return nil, errors.Wrap(err, "find") 78 | } 79 | return sandboxes, nil 80 | } 81 | 82 | type ListSandboxOptions struct { 83 | dbutil.Pagination 84 | } 85 | 86 | func (db *sandboxes) List(ctx context.Context, options ListSandboxOptions) ([]*Sandbox, int64, error) { 87 | var sandboxes []*Sandbox 88 | 89 | query := db.WithContext(ctx).Model(&Sandbox{}).Preload("Template") 90 | 91 | var total int64 92 | if err := query.Count(&total).Error; err != nil { 93 | return nil, 0, errors.Wrap(err, "count") 94 | } 95 | 96 | limit, offset := options.LimitOffset() 97 | if err := query.Limit(limit).Offset(offset).Find(&sandboxes).Error; err != nil { 98 | return nil, 0, errors.Wrap(err, "find") 99 | } 100 | return sandboxes, total, nil 101 | } 102 | 103 | type CreateSandboxOptions struct { 104 | Name string 105 | TemplateID uint 106 | Placeholder string 107 | Editable bool 108 | } 109 | 110 | func (db *sandboxes) Create(ctx context.Context, options CreateSandboxOptions) (*Sandbox, error) { 111 | sandbox := &Sandbox{ 112 | UID: randstr.String(10), 113 | Name: options.Name, 114 | TemplateID: options.TemplateID, 115 | Placeholder: options.Placeholder, 116 | Editable: options.Editable, 117 | } 118 | 119 | if err := db.WithContext(ctx).Create(sandbox).Error; err != nil { 120 | return nil, errors.Wrap(err, "create") 121 | } 122 | return sandbox, nil 123 | } 124 | 125 | type UpdateSandboxOptions struct { 126 | Name string 127 | TemplateID uint 128 | Placeholder string 129 | Editable bool 130 | } 131 | 132 | func (db *sandboxes) Update(ctx context.Context, id uint, options UpdateSandboxOptions) error { 133 | sandbox, err := db.GetByID(ctx, id) 134 | if err != nil { 135 | return errors.Wrap(err, "get by ID") 136 | } 137 | 138 | sandbox.Name = options.Name 139 | sandbox.TemplateID = options.TemplateID 140 | sandbox.Placeholder = options.Placeholder 141 | sandbox.Editable = options.Editable 142 | 143 | if err := db.WithContext(ctx).Save(sandbox).Error; err != nil { 144 | return errors.Wrap(err, "save") 145 | } 146 | return nil 147 | } 148 | 149 | func (db *sandboxes) Delete(ctx context.Context, id uint) error { 150 | _, err := db.GetByID(ctx, id) 151 | if err != nil { 152 | return errors.Wrap(err, "get by ID") 153 | } 154 | 155 | if err := db.WithContext(ctx).Delete(&Sandbox{}, "id = ?", id).Error; err != nil { 156 | return errors.Wrap(err, "delete") 157 | } 158 | return nil 159 | } 160 | -------------------------------------------------------------------------------- /internal/db/templates.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/lib/pq" 8 | "github.com/pkg/errors" 9 | "gorm.io/datatypes" 10 | "gorm.io/gorm" 11 | 12 | "github.com/wuhan005/Elaina/internal/dbutil" 13 | ) 14 | 15 | var _ TplStore = (*tpls)(nil) 16 | 17 | var Tpls TplStore 18 | 19 | type TplStore interface { 20 | // All returns all the templates. 21 | All(ctx context.Context) ([]*Tpl, error) 22 | // List returns the templates with the given options. 23 | List(ctx context.Context, options ListTplOptions) ([]*Tpl, int64, error) 24 | // GetByID returns a template with the given id. 25 | GetByID(ctx context.Context, id uint) (*Tpl, error) 26 | // Create creates a new template with the given options. 27 | Create(ctx context.Context, options CreateTplOptions) (*Tpl, error) 28 | // Update edits a new template with the given options. 29 | Update(ctx context.Context, id uint, options UpdateTplOptions) error 30 | // Delete deletes a template with the given id. 31 | Delete(ctx context.Context, id uint) error 32 | } 33 | 34 | func NewTplStore(db *gorm.DB) TplStore { 35 | return &tpls{db} 36 | } 37 | 38 | type tpls struct { 39 | *gorm.DB 40 | } 41 | 42 | type Tpl struct { 43 | dbutil.Model 44 | 45 | Name string `json:"name"` 46 | Language pq.StringArray `gorm:"type:text[]" json:"language"` 47 | 48 | // Limit 49 | Timeout int `json:"timeout"` 50 | MaxCPUs int64 `json:"maxCpus"` 51 | MaxMemory int64 `json:"maxMemory"` 52 | InternetAccess bool `json:"internetAccess"` 53 | DNS datatypes.JSON `gorm:"type:jsonb" json:"dns"` 54 | MaxContainer int64 `json:"maxContainer"` 55 | MaxContainerPerIP int64 `json:"maxContainerPerIp"` 56 | } 57 | 58 | var ErrTemplateNotFound = errors.New("template dose not exist") 59 | 60 | func (db *tpls) GetByID(ctx context.Context, id uint) (*Tpl, error) { 61 | var template Tpl 62 | 63 | if err := db.WithContext(ctx).Model(&Tpl{}).Where("id = ?", id).First(&template).Error; err != nil { 64 | if errors.Is(err, gorm.ErrRecordNotFound) { 65 | return nil, ErrTemplateNotFound 66 | } 67 | 68 | return nil, errors.Wrap(err, "first") 69 | } 70 | return &template, nil 71 | } 72 | 73 | func (db *tpls) All(ctx context.Context) ([]*Tpl, error) { 74 | var templates []*Tpl 75 | if err := db.WithContext(ctx).Model(&Tpl{}).Find(&templates).Error; err != nil { 76 | return nil, errors.Wrap(err, "find") 77 | } 78 | return templates, nil 79 | } 80 | 81 | type ListTplOptions struct { 82 | dbutil.Pagination 83 | } 84 | 85 | func (db *tpls) List(ctx context.Context, options ListTplOptions) ([]*Tpl, int64, error) { 86 | var templates []*Tpl 87 | 88 | query := db.WithContext(ctx).Model(&Tpl{}) 89 | 90 | var total int64 91 | if err := query.Count(&total).Error; err != nil { 92 | return nil, 0, errors.Wrap(err, "count") 93 | } 94 | 95 | limit, offset := options.LimitOffset() 96 | if err := query.Limit(limit).Offset(offset).Find(&templates).Error; err != nil { 97 | return nil, 0, errors.Wrap(err, "find") 98 | } 99 | return templates, total, nil 100 | } 101 | 102 | type CreateTplOptions struct { 103 | Name string 104 | Language []string 105 | Timeout int 106 | MaxCPUs int64 107 | MaxMemory int64 108 | InternetAccess bool 109 | DNS map[string]string 110 | MaxContainer int64 111 | MaxContainerPerIP int64 112 | } 113 | 114 | func (db *tpls) Create(ctx context.Context, options CreateTplOptions) (*Tpl, error) { 115 | dnsValue, err := json.Marshal(options.DNS) 116 | if err != nil { 117 | return nil, errors.Wrap(err, "marshal dns") 118 | } 119 | dns := datatypes.JSON{} 120 | if err := dns.Scan(dnsValue); err != nil { 121 | return nil, errors.Wrap(err, "marshal DNS JSONs") 122 | } 123 | 124 | tpl := &Tpl{ 125 | Name: options.Name, 126 | Language: pq.StringArray(options.Language), 127 | Timeout: options.Timeout, 128 | MaxCPUs: options.MaxCPUs, 129 | MaxMemory: options.MaxMemory, 130 | InternetAccess: options.InternetAccess, 131 | DNS: dns, 132 | MaxContainer: options.MaxContainer, 133 | MaxContainerPerIP: options.MaxContainerPerIP, 134 | } 135 | 136 | if err := db.WithContext(ctx).Create(tpl).Error; err != nil { 137 | return nil, errors.Wrap(err, "create") 138 | } 139 | return tpl, nil 140 | } 141 | 142 | type UpdateTplOptions struct { 143 | ID uint 144 | Name string 145 | Language []string 146 | Timeout int 147 | MaxCPUs int64 148 | MaxMemory int64 149 | InternetAccess bool 150 | DNS map[string]string 151 | MaxContainer int64 152 | MaxContainerPerIP int64 153 | } 154 | 155 | func (db *tpls) Update(ctx context.Context, id uint, options UpdateTplOptions) error { 156 | template, err := db.GetByID(ctx, id) 157 | if err != nil { 158 | return errors.Wrap(err, "get by ID") 159 | } 160 | 161 | dnsValue, err := json.Marshal(options.DNS) 162 | if err != nil { 163 | return errors.Wrap(err, "marshal dns") 164 | } 165 | dns := datatypes.JSON{} 166 | if err := dns.Scan(dnsValue); err != nil { 167 | return errors.Wrap(err, "marshal DNS JSONs") 168 | } 169 | 170 | template.Name = options.Name 171 | template.Language = options.Language 172 | template.Timeout = options.Timeout 173 | template.MaxCPUs = options.MaxCPUs 174 | template.MaxMemory = options.MaxMemory 175 | template.InternetAccess = options.InternetAccess 176 | template.DNS = dns 177 | template.MaxContainer = options.MaxContainer 178 | template.MaxContainerPerIP = options.MaxContainerPerIP 179 | 180 | if err := db.WithContext(ctx).Save(template).Error; err != nil { 181 | return errors.Wrap(err, "save") 182 | } 183 | return nil 184 | } 185 | 186 | func (db *tpls) Delete(ctx context.Context, id uint) error { 187 | _, err := db.GetByID(ctx, id) 188 | if err != nil { 189 | return errors.Wrap(err, "get by ID") 190 | } 191 | 192 | if err := db.WithContext(ctx).Delete(&Tpl{}, "id = ?", id).Error; err != nil { 193 | return errors.Wrap(err, "delete") 194 | } 195 | return nil 196 | } 197 | -------------------------------------------------------------------------------- /internal/dbutil/db.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package dbutil 6 | 7 | import ( 8 | "database/sql" 9 | 10 | "gorm.io/gorm" 11 | ) 12 | 13 | type Transactor interface { 14 | Transaction(fc func(tx *gorm.DB) error, opts ...*sql.TxOptions) (err error) 15 | } 16 | -------------------------------------------------------------------------------- /internal/dbutil/model.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package dbutil 6 | 7 | import ( 8 | "time" 9 | 10 | "gorm.io/gorm" 11 | ) 12 | 13 | type Model struct { 14 | ID uint `gorm:"primarykey" json:"id"` 15 | CreatedAt time.Time `json:"createdAt"` 16 | UpdatedAt time.Time `json:"updatedAt"` 17 | DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` 18 | } 19 | -------------------------------------------------------------------------------- /internal/dbutil/pagination.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package dbutil 6 | 7 | var DefaultPageSize = 20 8 | 9 | type Pagination struct { 10 | Page int 11 | PageSize int 12 | } 13 | 14 | func (p Pagination) Normalize() Pagination { 15 | if p.Page <= 0 { 16 | p.Page = 1 17 | } 18 | if p.PageSize <= 0 { 19 | p.PageSize = DefaultPageSize 20 | } 21 | return p 22 | } 23 | 24 | func (p Pagination) LimitOffset() (limit, offset int) { 25 | return LimitOffset(p.Page, p.PageSize) 26 | } 27 | 28 | // LimitOffset returns LIMIT and OFFSET parameter for SQL. 29 | // The first page is page 0. 30 | func LimitOffset(page, pageSize int) (limit, offset int) { 31 | if page <= 0 { 32 | page = 1 33 | } 34 | if pageSize < 1 { 35 | pageSize = DefaultPageSize 36 | } 37 | return pageSize, (page - 1) * pageSize 38 | } 39 | -------------------------------------------------------------------------------- /internal/form/auth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package form 6 | 7 | type SignIn struct { 8 | Password string `json:"password" valid:"required"` 9 | } 10 | -------------------------------------------------------------------------------- /internal/form/form.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package form 6 | 7 | import ( 8 | "encoding/json" 9 | "net/http" 10 | "reflect" 11 | 12 | "github.com/flamego/flamego" 13 | "github.com/wuhan005/govalid" 14 | 15 | "github.com/wuhan005/Elaina/internal/context" 16 | ) 17 | 18 | func Bind(model interface{}) flamego.Handler { 19 | // Ensure not pointer. 20 | if reflect.TypeOf(model).Kind() == reflect.Ptr { 21 | panic("form: pointer can not be accepted as binding model") 22 | } 23 | 24 | return func(ctx context.Context) { 25 | obj := reflect.New(reflect.TypeOf(model)) 26 | r := ctx.Request().Request 27 | if r.Body != nil { 28 | defer func() { _ = r.Body.Close() }() 29 | 30 | if err := json.NewDecoder(r.Body).Decode(obj.Interface()); err != nil { 31 | _ = ctx.Error(http.StatusBadRequest, "Failed to parse request body") 32 | return 33 | } 34 | } 35 | 36 | errors, ok := govalid.Check(obj.Interface()) 37 | if !ok { 38 | _ = ctx.Error(http.StatusBadRequest, errors[0].Error()) 39 | return 40 | } 41 | 42 | // Validation passed. 43 | ctx.Map(obj.Elem().Interface()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/form/sandbox.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package form 6 | 7 | type CreateSandbox struct { 8 | Name string `json:"name" valid:"required"` 9 | TemplateID uint `json:"templateID" valid:"required"` 10 | Placeholder string `json:"placeholder"` 11 | Editable bool `json:"editable"` 12 | } 13 | 14 | type UpdateSandbox struct { 15 | Name string `json:"name" valid:"required"` 16 | TemplateID uint `json:"templateID" valid:"required"` 17 | Placeholder string `json:"placeholder"` 18 | Editable bool `json:"editable"` 19 | } 20 | -------------------------------------------------------------------------------- /internal/form/template.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package form 6 | 7 | type CreateTemplate struct { 8 | Name string `json:"name" valid:"required"` 9 | Language []string `json:"language" valid:"required"` 10 | Timeout int `json:"timeout" valid:"required;min:0;max:60"` 11 | MaxCPUs int64 `json:"maxCpus" valid:"required;min:0;max:10"` 12 | MaxMemory int64 `json:"maxMemory" valid:"required;min:6;max:2048"` 13 | InternetAccess bool `json:"internetAccess"` 14 | DNS map[string]string `json:"dns"` 15 | MaxContainer int64 `json:"maxContainer" valid:"required;min:0;max:1000"` 16 | MaxContainerPerIP int64 `json:"maxContainerPerIP" valid:"required;min:0;max:100"` 17 | } 18 | 19 | type UpdateTemplate struct { 20 | Name string `json:"name" valid:"required"` 21 | Language []string `json:"language" valid:"required"` 22 | Timeout int `json:"timeout" valid:"required;min:0;max:60"` 23 | MaxCPUs int64 `json:"maxCpus" valid:"required;min:0;max:10"` 24 | MaxMemory int64 `json:"maxMemory" valid:"required;min:6;max:2048"` 25 | InternetAccess bool `json:"internetAccess"` 26 | DNS map[string]string `json:"dns"` 27 | MaxContainer int64 `json:"maxContainer" valid:"required;min:0;max:1000"` 28 | MaxContainerPerIP int64 `json:"maxContainerPerIP" valid:"required;min:0;max:100"` 29 | } 30 | -------------------------------------------------------------------------------- /internal/languages/languages.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package languages 6 | 7 | var runners = []Runner{ 8 | { 9 | Name: "php", 10 | Image: "glot/php:latest", 11 | FileName: "main.php", 12 | BuildCommands: nil, 13 | RunCommand: "php main.php", 14 | }, 15 | { 16 | Name: "python", 17 | Image: "glot/python:latest", 18 | FileName: "main.py", 19 | BuildCommands: nil, 20 | RunCommand: "python main.py", 21 | }, 22 | { 23 | Name: "go", 24 | Image: "glot/golang:latest", 25 | FileName: "main.go", 26 | BuildCommands: nil, 27 | RunCommand: "go run main.go", 28 | }, 29 | { 30 | Name: "javascript", 31 | Image: "glot/javascript:latest", 32 | FileName: "main.js", 33 | BuildCommands: nil, 34 | RunCommand: "node main.js", 35 | }, 36 | { 37 | Name: "javascript", 38 | Image: "glot/c:latest", 39 | FileName: "main.c", 40 | BuildCommands: nil, 41 | RunCommand: "clang main.c && ./a.out", 42 | }, 43 | } 44 | 45 | func Get(name string) (*Runner, bool) { 46 | for _, r := range runners { 47 | r := r 48 | if r.Name == name { 49 | return &r, true 50 | } 51 | } 52 | 53 | return nil, false 54 | } 55 | -------------------------------------------------------------------------------- /internal/languages/runner.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package languages 6 | 7 | type Runner struct { 8 | Name string 9 | Image string 10 | FileName string 11 | BuildCommands []string 12 | RunCommand string 13 | } 14 | -------------------------------------------------------------------------------- /internal/ratelimit/ratelimit.go: -------------------------------------------------------------------------------- 1 | package ratelimit 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | ) 7 | 8 | var RateLimitError = errors.New("rate limit") 9 | 10 | var rate = rateLimit{ 11 | buckets: map[string]*bucket{}, 12 | } 13 | 14 | type rateLimit struct { 15 | sync.RWMutex 16 | 17 | buckets map[string]*bucket 18 | } 19 | 20 | type bucket struct { 21 | sync.RWMutex 22 | 23 | key string 24 | data int64 25 | } 26 | 27 | func Add(key string, max int64) error { 28 | rate.Lock() 29 | defer rate.Unlock() 30 | 31 | bkt, ok := rate.buckets[key] 32 | if !ok { 33 | if max <= 0 { 34 | return RateLimitError 35 | } 36 | rate.buckets[key] = &bucket{ 37 | RWMutex: sync.RWMutex{}, 38 | key: key, 39 | data: 1, 40 | } 41 | return nil 42 | } 43 | 44 | bkt.Lock() 45 | defer bkt.Unlock() 46 | if bkt.data+1 > max { 47 | return RateLimitError 48 | } 49 | bkt.data++ 50 | return nil 51 | } 52 | 53 | func Get(key string) int64 { 54 | rate.RLock() 55 | defer rate.Unlock() 56 | 57 | bkt, ok := rate.buckets[key] 58 | if !ok { 59 | return 0 60 | } 61 | 62 | bkt.RLock() 63 | defer bkt.RUnlock() 64 | return bkt.data 65 | } 66 | 67 | func Done(key string) { 68 | rate.Lock() 69 | defer rate.Unlock() 70 | 71 | bkt, ok := rate.buckets[key] 72 | if !ok { 73 | return 74 | } 75 | 76 | bkt.Lock() 77 | defer bkt.Unlock() 78 | bkt.data-- 79 | if bkt.data < 0 { 80 | bkt.data = 0 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/route/auth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package route 6 | 7 | import ( 8 | "crypto/subtle" 9 | "net/http" 10 | 11 | "github.com/flamego/session" 12 | 13 | "github.com/wuhan005/Elaina/internal/config" 14 | "github.com/wuhan005/Elaina/internal/context" 15 | "github.com/wuhan005/Elaina/internal/form" 16 | ) 17 | 18 | type AuthHandler struct{} 19 | 20 | func NewAuthHandler() *AuthHandler { 21 | return &AuthHandler{} 22 | } 23 | 24 | func (h *AuthHandler) Authenticator(ctx context.Context) error { 25 | if !ctx.IsAuthenticated { 26 | return ctx.Error(http.StatusUnauthorized, "Unauthorized") 27 | } 28 | return nil 29 | } 30 | 31 | func (h *AuthHandler) SignIn(ctx context.Context, session session.Session, f form.SignIn) error { 32 | appPassword := config.App.Password 33 | if subtle.ConstantTimeCompare([]byte(appPassword), []byte(f.Password)) != 1 { 34 | return ctx.Error(http.StatusForbidden, "Invalid password") 35 | } 36 | 37 | session.Set(context.SessionIDIsAuthenticated, true) 38 | 39 | return ctx.Success(session.ID()) 40 | } 41 | 42 | func (h *AuthHandler) Profile(ctx context.Context) error { 43 | return ctx.Success() 44 | } 45 | 46 | func (h *AuthHandler) SignOut(ctx context.Context, session session.Session) error { 47 | session.Flush() 48 | return ctx.Success("Sign out successfully") 49 | } 50 | -------------------------------------------------------------------------------- /internal/route/frontend.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package route 6 | 7 | import ( 8 | "net/http" 9 | 10 | "github.com/wuhan005/Elaina/internal/context" 11 | "github.com/wuhan005/Elaina/web" 12 | ) 13 | 14 | func Frontend(c context.Context) { 15 | if c.Request().Method != http.MethodGet && c.Request().Method != http.MethodHead { 16 | return 17 | } 18 | 19 | name := "dist/index.html" 20 | f, err := http.FS(web.FS).Open(name) 21 | if err != nil { 22 | return 23 | } 24 | defer func() { _ = f.Close() }() 25 | 26 | fi, err := f.Stat() 27 | if err != nil { 28 | return // File exists but failed to open. 29 | } 30 | 31 | http.ServeContent(c.ResponseWriter(), c.Request().Request, name, fi.ModTime(), f) 32 | } 33 | -------------------------------------------------------------------------------- /internal/route/route.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "io/fs" 5 | "net/http" 6 | 7 | "github.com/flamego/flamego" 8 | "github.com/flamego/session" 9 | "github.com/flamego/template" 10 | "github.com/pkg/errors" 11 | "gorm.io/gorm" 12 | 13 | "github.com/wuhan005/Elaina/internal/context" 14 | "github.com/wuhan005/Elaina/internal/form" 15 | "github.com/wuhan005/Elaina/public" 16 | "github.com/wuhan005/Elaina/templates" 17 | "github.com/wuhan005/Elaina/web" 18 | ) 19 | 20 | // New returns a new Flamego router. 21 | func New(db *gorm.DB) (*flamego.Flame, error) { 22 | f := flamego.Classic() 23 | 24 | frontendFS, err := fs.Sub(web.FS, "dist") 25 | if err != nil { 26 | return nil, errors.Wrap(err, "fs sub") 27 | } 28 | 29 | templatesFS, err := template.EmbedFS(templates.FS, ".", []string{".tmpl"}) 30 | if err != nil { 31 | return nil, errors.Wrap(err, "embed templates fs") 32 | } 33 | 34 | f.Use( 35 | session.Sessioner(session.Options{ 36 | // TODO: support postgresql 37 | Initer: session.MemoryIniter(), 38 | ReadIDFunc: context.ReadIDFunc, 39 | WriteIDFunc: context.WriteIDFunc, 40 | }), 41 | 42 | // Public static files. 43 | flamego.Static(flamego.StaticOptions{ 44 | FileSystem: http.FS(public.FS), 45 | Prefix: "static", 46 | }), 47 | // Frontend static files. 48 | flamego.Static(flamego.StaticOptions{ 49 | FileSystem: http.FS(frontendFS), 50 | }), 51 | template.Templater(template.Options{ 52 | FileSystem: templatesFS, 53 | }), 54 | context.Contexter(db), 55 | ) 56 | 57 | runnerHandler := NewRunnerHandler() 58 | f.Group("/r/{uid}", func() { 59 | f.Combo("").Get(runnerHandler.View).Post(runnerHandler.View) 60 | f.Post("/execute", runnerHandler.Execute) 61 | }, runnerHandler.Runner) 62 | 63 | f.Group("/api", func() { 64 | authHandler := NewAuthHandler() 65 | f.Post("/auth/sign-in", form.Bind(form.SignIn{}), authHandler.SignIn) 66 | 67 | f.Group("", func() { 68 | f.Group("/auth", func() { 69 | f.Post("/sign-out", authHandler.SignOut) 70 | f.Get("/profile", authHandler.Profile) 71 | }) 72 | 73 | templateHandler := NewTemplateHandler() 74 | f.Combo("/templates"). 75 | Get(templateHandler.List). 76 | Post(form.Bind(form.CreateTemplate{}), templateHandler.Create) 77 | f.Get("/templates/all", templateHandler.All) 78 | f.Group("/template/{id}", func() { 79 | f.Combo(""). 80 | Get(templateHandler.Get). 81 | Put(form.Bind(form.UpdateTemplate{}), templateHandler.Update). 82 | Delete(templateHandler.Delete) 83 | }, templateHandler.Templater) 84 | 85 | sandboxHandler := NewSandboxHandler() 86 | f.Combo("/sandboxes"). 87 | Get(sandboxHandler.List). 88 | Post(form.Bind(form.CreateSandbox{}), sandboxHandler.Create) 89 | f.Group("/sandbox/{id}", func() { 90 | f.Combo(""). 91 | Get(sandboxHandler.Get). 92 | Put(form.Bind(form.UpdateSandbox{}), sandboxHandler.Update). 93 | Delete(sandboxHandler.Delete) 94 | }, sandboxHandler.Sandboxer) 95 | }, authHandler.Authenticator) 96 | }) 97 | 98 | f.NotFound(Frontend) 99 | 100 | f.Get("/healthz") 101 | 102 | return f, nil 103 | } 104 | -------------------------------------------------------------------------------- /internal/route/runner.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package route 6 | 7 | import ( 8 | "net/http" 9 | "time" 10 | 11 | "github.com/flamego/template" 12 | "github.com/samber/lo" 13 | "github.com/sirupsen/logrus" 14 | 15 | "github.com/wuhan005/Elaina/internal/config" 16 | "github.com/wuhan005/Elaina/internal/context" 17 | "github.com/wuhan005/Elaina/internal/db" 18 | "github.com/wuhan005/Elaina/internal/runtime" 19 | ) 20 | 21 | type RunnerHandler struct{} 22 | 23 | func NewRunnerHandler() *RunnerHandler { 24 | return &RunnerHandler{} 25 | } 26 | 27 | func (h *RunnerHandler) Runner(ctx context.Context) error { 28 | sandboxUID := ctx.Param("uid") 29 | sandbox, err := db.Sandboxes.GetByUID(ctx.Request().Context(), sandboxUID) 30 | if err != nil { 31 | ctx.Redirect("/") 32 | return nil 33 | } 34 | 35 | ctx.Map(sandbox) 36 | return nil 37 | } 38 | 39 | func (h *RunnerHandler) View(ctx context.Context, sandbox *db.Sandbox, t template.Template, data template.Data) { 40 | languages := sandbox.Template.Language 41 | selectedLanguage := ctx.Query("l") 42 | if !lo.Contains(languages, selectedLanguage) { 43 | selectedLanguage = languages[0] 44 | } 45 | 46 | _ = ctx.Request().ParseForm() 47 | code := ctx.Request().PostForm.Get("c") 48 | if code == "" { 49 | code = sandbox.Placeholder 50 | } 51 | 52 | data["Sandbox"] = sandbox 53 | data["Language"] = selectedLanguage 54 | data["Languages"] = languages 55 | data["Code"] = code 56 | 57 | t.HTML(http.StatusOK, "sandbox") 58 | } 59 | 60 | func (h *RunnerHandler) Execute(ctx context.Context, sandbox *db.Sandbox) error { 61 | if err := ctx.Request().ParseForm(); err != nil { 62 | logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to parse form") 63 | return ctx.Error(http.StatusBadRequest, "Failed to parse form: %v", err) 64 | } 65 | 66 | templateLanguages := sandbox.Template.Language 67 | selectedLanguage := ctx.Request().PostForm.Get("lang") 68 | if !lo.Contains(templateLanguages, selectedLanguage) { 69 | selectedLanguage = templateLanguages[0] 70 | } 71 | code := ctx.Request().PostForm.Get("code") 72 | 73 | // TODO: Rate limit 74 | 75 | startAt := time.Now().UnixNano() 76 | 77 | var r runtime.ExecRuntime 78 | var err error 79 | 80 | switch config.App.RuntimeMode { 81 | case "kubernetes", "k8s": 82 | r, err = runtime.NewKubernetesTask(ctx.Request().Context(), runtime.NewKubernetesTaskOptions{ 83 | Language: selectedLanguage, 84 | Template: sandbox.Template, 85 | Code: []byte(code), 86 | }) 87 | 88 | case "docker", "": 89 | r, err = runtime.NewDockerTask(ctx.Request().Context(), runtime.NewDockerTaskOptions{ 90 | Language: selectedLanguage, 91 | Template: sandbox.Template, 92 | Code: []byte(code), 93 | }) 94 | 95 | default: 96 | return ctx.Error(http.StatusInternalServerError, "unexpected runtime mode: %q", config.App.RuntimeMode) 97 | } 98 | 99 | if err != nil { 100 | logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to create task") 101 | return ctx.Error(http.StatusInternalServerError, "Failed to create task: %v", err) 102 | } 103 | 104 | output, err := r.Run(ctx.Request().Context()) 105 | if err != nil { 106 | logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to run task") 107 | return ctx.Error(http.StatusInternalServerError, "Failed to run task: %v", err) 108 | } 109 | 110 | endAt := time.Now().UnixNano() 111 | 112 | return ctx.Success(map[string]interface{}{ 113 | "result": output, 114 | "start_at": startAt, 115 | "end_at": endAt, 116 | }) 117 | } 118 | -------------------------------------------------------------------------------- /internal/route/sandbox.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package route 6 | 7 | import ( 8 | "net/http" 9 | 10 | "github.com/pkg/errors" 11 | "github.com/sirupsen/logrus" 12 | 13 | "github.com/wuhan005/Elaina/internal/context" 14 | "github.com/wuhan005/Elaina/internal/db" 15 | "github.com/wuhan005/Elaina/internal/dbutil" 16 | "github.com/wuhan005/Elaina/internal/form" 17 | ) 18 | 19 | type SandboxHandler struct{} 20 | 21 | func NewSandboxHandler() *SandboxHandler { 22 | return &SandboxHandler{} 23 | } 24 | 25 | func (h *SandboxHandler) Sandboxer(ctx context.Context) error { 26 | sandboxID := uint(ctx.ParamInt("id")) 27 | 28 | sandbox, err := db.Sandboxes.GetByID(ctx.Request().Context(), sandboxID) 29 | if err != nil { 30 | if errors.Is(err, db.ErrSandboxNotFound) { 31 | return ctx.Error(http.StatusNotFound, "Sanbox does not exist") 32 | } 33 | logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to get sandbox") 34 | return ctx.ServerError() 35 | } 36 | 37 | ctx.Map(sandbox) 38 | return nil 39 | } 40 | 41 | func (h *SandboxHandler) List(ctx context.Context) error { 42 | sandboxes, total, err := db.Sandboxes.List(ctx.Request().Context(), db.ListSandboxOptions{ 43 | Pagination: dbutil.Pagination{ 44 | Page: ctx.QueryInt("page"), 45 | PageSize: ctx.QueryInt("pageSize"), 46 | }, 47 | }) 48 | if err != nil { 49 | logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to list sandboxes") 50 | return ctx.ServerError() 51 | } 52 | 53 | return ctx.Success(map[string]interface{}{ 54 | "sandboxes": sandboxes, 55 | "total": total, 56 | }) 57 | } 58 | 59 | func (h *SandboxHandler) Get(ctx context.Context, sandbox *db.Sandbox) error { 60 | return ctx.Success(sandbox) 61 | } 62 | 63 | func (h *SandboxHandler) Create(ctx context.Context, f form.CreateSandbox) error { 64 | sandbox, err := db.Sandboxes.Create(ctx.Request().Context(), db.CreateSandboxOptions{ 65 | Name: f.Name, 66 | TemplateID: f.TemplateID, 67 | Placeholder: f.Placeholder, 68 | Editable: f.Editable, 69 | }) 70 | if err != nil { 71 | logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to create sandbox") 72 | return ctx.ServerError() 73 | } 74 | 75 | return ctx.Success(sandbox) 76 | } 77 | 78 | func (h *SandboxHandler) Update(ctx context.Context, sandbox *db.Sandbox, f form.UpdateSandbox) error { 79 | sandboxID := sandbox.ID 80 | 81 | if err := db.Sandboxes.Update(ctx.Request().Context(), sandboxID, db.UpdateSandboxOptions{ 82 | Name: f.Name, 83 | TemplateID: f.TemplateID, 84 | Placeholder: f.Placeholder, 85 | Editable: f.Editable, 86 | }); err != nil { 87 | logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to update sandbox") 88 | return ctx.ServerError() 89 | } 90 | 91 | return ctx.Success("SandBox updated successfully") 92 | } 93 | 94 | func (h *SandboxHandler) Delete(ctx context.Context, sandbox *db.Sandbox) error { 95 | sandboxID := sandbox.ID 96 | 97 | if err := db.Sandboxes.Delete(ctx.Request().Context(), sandboxID); err != nil { 98 | if errors.Is(err, db.ErrSandboxNotFound) { 99 | return ctx.Error(http.StatusNotFound, "Sandbox does not exist") 100 | } 101 | logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to delete sandbox") 102 | return ctx.ServerError() 103 | } 104 | return ctx.Success("Sandbox deleted successfully") 105 | } 106 | -------------------------------------------------------------------------------- /internal/route/template.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package route 6 | 7 | import ( 8 | "net/http" 9 | 10 | "github.com/pkg/errors" 11 | "github.com/sirupsen/logrus" 12 | 13 | "github.com/wuhan005/Elaina/internal/context" 14 | "github.com/wuhan005/Elaina/internal/db" 15 | "github.com/wuhan005/Elaina/internal/dbutil" 16 | "github.com/wuhan005/Elaina/internal/form" 17 | ) 18 | 19 | type TemplateHandler struct{} 20 | 21 | func NewTemplateHandler() *TemplateHandler { 22 | return &TemplateHandler{} 23 | } 24 | 25 | func (h *TemplateHandler) Templater(ctx context.Context) error { 26 | templateID := uint(ctx.ParamInt("id")) 27 | tpl, err := db.Tpls.GetByID(ctx.Request().Context(), templateID) 28 | if err != nil { 29 | if errors.Is(err, db.ErrTemplateNotFound) { 30 | return ctx.Error(http.StatusNotFound, "Template dose not exist") 31 | } 32 | logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to get template") 33 | return ctx.ServerError() 34 | } 35 | 36 | ctx.Map(tpl) 37 | return nil 38 | } 39 | 40 | func (h *TemplateHandler) All(ctx context.Context) error { 41 | templates, err := db.Tpls.All(ctx.Request().Context()) 42 | if err != nil { 43 | logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to get all templates") 44 | return ctx.ServerError() 45 | } 46 | return ctx.Success(templates) 47 | } 48 | 49 | func (h *TemplateHandler) List(ctx context.Context) error { 50 | templates, total, err := db.Tpls.List(ctx.Request().Context(), db.ListTplOptions{ 51 | Pagination: dbutil.Pagination{ 52 | Page: ctx.QueryInt("page"), 53 | PageSize: ctx.QueryInt("pageSize"), 54 | }, 55 | }) 56 | if err != nil { 57 | logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to list templates") 58 | return ctx.ServerError() 59 | } 60 | 61 | return ctx.Success(map[string]interface{}{ 62 | "templates": templates, 63 | "total": total, 64 | }) 65 | } 66 | 67 | func (h *TemplateHandler) Get(ctx context.Context, template *db.Tpl) error { 68 | return ctx.Success(template) 69 | } 70 | 71 | func (h *TemplateHandler) Create(ctx context.Context, f form.CreateTemplate) error { 72 | template, err := db.Tpls.Create(ctx.Request().Context(), db.CreateTplOptions{ 73 | Name: f.Name, 74 | Language: f.Language, 75 | Timeout: f.Timeout, 76 | MaxCPUs: f.MaxCPUs, 77 | MaxMemory: f.MaxMemory, 78 | InternetAccess: f.InternetAccess, 79 | DNS: f.DNS, 80 | MaxContainer: f.MaxContainer, 81 | MaxContainerPerIP: f.MaxContainerPerIP, 82 | }) 83 | if err != nil { 84 | logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to create template") 85 | return ctx.ServerError() 86 | } 87 | return ctx.Success(template) 88 | } 89 | 90 | func (h *TemplateHandler) Update(ctx context.Context, template *db.Tpl, f form.UpdateTemplate) error { 91 | templateID := template.ID 92 | 93 | if err := db.Tpls.Update(ctx.Request().Context(), templateID, db.UpdateTplOptions{ 94 | Name: f.Name, 95 | Language: f.Language, 96 | Timeout: f.Timeout, 97 | MaxCPUs: f.MaxCPUs, 98 | MaxMemory: f.MaxMemory, 99 | InternetAccess: f.InternetAccess, 100 | DNS: f.DNS, 101 | MaxContainer: f.MaxContainer, 102 | MaxContainerPerIP: f.MaxContainerPerIP, 103 | }); err != nil { 104 | if errors.Is(err, db.ErrTemplateNotFound) { 105 | return ctx.Error(http.StatusNotFound, "Template dose not exist") 106 | } 107 | logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to update template") 108 | return ctx.ServerError() 109 | } 110 | return ctx.Success("Template updated successfully") 111 | } 112 | 113 | func (h *TemplateHandler) Delete(ctx context.Context, template *db.Tpl) error { 114 | templateID := template.ID 115 | 116 | if err := db.Tpls.Delete(ctx.Request().Context(), templateID); err != nil { 117 | if errors.Is(err, db.ErrTemplateNotFound) { 118 | return ctx.Error(http.StatusNotFound, "Template dose not exist") 119 | } 120 | logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to delete template") 121 | return ctx.ServerError() 122 | } 123 | return ctx.Success("Template deleted successfully") 124 | } 125 | -------------------------------------------------------------------------------- /internal/runtime/docker.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "encoding/json" 7 | "io" 8 | 9 | "github.com/docker/docker/api/types/container" 10 | "github.com/docker/docker/client" 11 | "github.com/pkg/errors" 12 | "github.com/satori/go.uuid" 13 | "github.com/sirupsen/logrus" 14 | 15 | "github.com/wuhan005/Elaina/internal/db" 16 | "github.com/wuhan005/Elaina/internal/languages" 17 | ) 18 | 19 | type DockerTask struct { 20 | uuid string 21 | containerID string 22 | 23 | runner *languages.Runner 24 | template *db.Tpl 25 | code []byte 26 | 27 | dockerClient *client.Client 28 | } 29 | 30 | type NewDockerTaskOptions struct { 31 | Language string 32 | Template *db.Tpl 33 | Code []byte 34 | } 35 | 36 | // NewDockerTask creates a new task based on the given code and ready for execution. 37 | func NewDockerTask(ctx context.Context, options NewDockerTaskOptions) (*DockerTask, error) { 38 | uid := uuid.NewV4().String() 39 | language := options.Language 40 | template := options.Template 41 | code := options.Code 42 | 43 | // Set the programming language runner. 44 | runner, ok := languages.Get(language) 45 | if !ok { 46 | return nil, errors.Errorf("unexpected language: %q", language) 47 | } 48 | 49 | // Create a new docker client. 50 | dockerClient, err := client.NewClientWithOpts() 51 | if err != nil { 52 | return nil, errors.Wrap(err, "new docker client") 53 | } 54 | dockerClient.NegotiateAPIVersion(ctx) 55 | 56 | return &DockerTask{ 57 | uuid: uid, 58 | runner: runner, 59 | template: template, 60 | code: code, 61 | dockerClient: dockerClient, 62 | }, nil 63 | } 64 | 65 | // Run runs a task. 66 | func (t *DockerTask) Run(ctx context.Context) (*ExecOutput, error) { 67 | createContainerResp, err := t.dockerClient.ContainerCreate(ctx, 68 | &container.Config{ 69 | Hostname: "elaina-runtime", 70 | AttachStdin: true, 71 | AttachStdout: true, 72 | AttachStderr: true, 73 | Tty: false, 74 | OpenStdin: true, 75 | StdinOnce: true, 76 | Image: t.runner.Image, 77 | NetworkDisabled: !t.template.InternetAccess, 78 | Env: nil, // TODO 79 | }, 80 | &container.HostConfig{ 81 | Resources: container.Resources{ 82 | NanoCPUs: t.template.MaxCPUs * 1000000000, // 0.000000001 * CPU of cpu 83 | Memory: t.template.MaxMemory * 1024 * 1024, // Minimum memory limit allowed is 6MB. 84 | }, 85 | }, nil, nil, t.uuid) 86 | if err != nil { 87 | return nil, errors.Wrap(err, "create container") 88 | } 89 | 90 | t.containerID = createContainerResp.ID 91 | 92 | // Clean containers and folder after executed. 93 | defer t.clean(ctx) 94 | 95 | if err := t.dockerClient.ContainerStart(ctx, t.containerID, container.StartOptions{}); err != nil { 96 | return nil, errors.Wrap(err, "start container") 97 | } 98 | 99 | // Execute code. 100 | output, err := t.exec(ctx, string(t.code)) 101 | if err != nil { 102 | return nil, errors.Wrap(err, "exec") 103 | } 104 | return output, nil 105 | } 106 | 107 | func (t *DockerTask) exec(ctx context.Context, code string) (*ExecOutput, error) { 108 | attach, err := t.dockerClient.ContainerAttach(ctx, t.containerID, container.AttachOptions{ 109 | Stream: true, 110 | Stdin: true, 111 | Stdout: true, 112 | Stderr: true, 113 | Logs: false, 114 | }) 115 | if err != nil { 116 | return nil, errors.Wrap(err, "attach") 117 | } 118 | defer func() { attach.Close() }() 119 | 120 | buildCommands := t.runner.BuildCommands 121 | if buildCommands == nil { 122 | buildCommands = []string{} 123 | } 124 | 125 | input := ExecInput{ 126 | RunInstructions: ExecInputRunInstructions{ 127 | BuildCommands: buildCommands, 128 | RunCommand: t.runner.RunCommand, 129 | }, 130 | Files: []*ExecInputFile{ 131 | { 132 | Name: t.runner.FileName, 133 | Content: code, 134 | }, 135 | }, 136 | Stdin: nil, 137 | } 138 | 139 | if err := json.NewEncoder(attach.Conn).Encode(input); err != nil { 140 | return nil, errors.Wrap(err, "write") 141 | } 142 | 143 | // Send an `EOF` to writer. 144 | if err := attach.CloseWrite(); err != nil { 145 | return nil, errors.Wrap(err, "close write") 146 | } 147 | 148 | output, err := io.ReadAll(attach.Reader) 149 | if err != nil { 150 | return nil, errors.Wrap(err, "read output") 151 | } 152 | output = parseDockerLog(output) 153 | 154 | var execOutput ExecOutput 155 | if err := json.Unmarshal(output, &execOutput); err != nil { 156 | return nil, errors.Wrap(err, "unmarshal output") 157 | } 158 | return &execOutput, nil 159 | } 160 | 161 | func (t *DockerTask) clean(ctx context.Context) { 162 | if err := t.dockerClient.ContainerRemove(ctx, t.containerID, container.RemoveOptions{ 163 | Force: true, 164 | }); err != nil { 165 | logrus.WithContext(ctx).WithError(err).Error("Failed to remove container") 166 | } 167 | } 168 | 169 | // parseDockerLog parse the header of the docker logs. 170 | // More information at: https://github.com/moby/moby/issues/7375#issuecomment-51462963 171 | // 172 | // header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} 173 | func parseDockerLog(logs []byte) []byte { 174 | output := make([]byte, 0, len(logs)) 175 | 176 | for i := 0; i < len(logs); { 177 | sizeBinary := logs[i+4 : i+8] 178 | i += 8 179 | 180 | size := int(binary.BigEndian.Uint32(sizeBinary)) 181 | data := logs[i : i+size] 182 | output = append(output, data...) 183 | i += size 184 | } 185 | 186 | return output 187 | } 188 | -------------------------------------------------------------------------------- /internal/runtime/kubernetes.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package runtime 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "encoding/json" 11 | "net/http" 12 | "time" 13 | 14 | "github.com/pkg/errors" 15 | uuid "github.com/satori/go.uuid" 16 | "github.com/sirupsen/logrus" 17 | "github.com/wuhan005/gadget" 18 | v1 "k8s.io/api/core/v1" 19 | "k8s.io/apimachinery/pkg/api/resource" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | "k8s.io/apimachinery/pkg/util/wait" 22 | "k8s.io/client-go/kubernetes" 23 | "k8s.io/client-go/kubernetes/scheme" 24 | "k8s.io/client-go/rest" 25 | "k8s.io/client-go/tools/remotecommand" 26 | 27 | "github.com/wuhan005/Elaina/internal/config" 28 | "github.com/wuhan005/Elaina/internal/db" 29 | "github.com/wuhan005/Elaina/internal/languages" 30 | ) 31 | 32 | type KubernetesTask struct { 33 | config *rest.Config 34 | k8sClient *kubernetes.Clientset 35 | runner *languages.Runner 36 | 37 | language string 38 | template *db.Tpl 39 | code []byte 40 | } 41 | 42 | type NewKubernetesTaskOptions struct { 43 | Language string 44 | Template *db.Tpl 45 | Code []byte 46 | } 47 | 48 | func NewKubernetesTask(_ context.Context, options NewKubernetesTaskOptions) (*KubernetesTask, error) { 49 | language := options.Language 50 | template := options.Template 51 | code := options.Code 52 | 53 | // Set the programming language runner. 54 | runner, ok := languages.Get(language) 55 | if !ok { 56 | return nil, errors.Errorf("unexpected language: %q", language) 57 | } 58 | 59 | caData := []byte(gadget.Base64Decode(config.App.KubernetesCAData)) 60 | certData := []byte(gadget.Base64Decode(config.App.KubernetesCertData)) 61 | keyData := []byte(gadget.Base64Decode(config.App.KubernetesKeyData)) 62 | bearerToken := config.App.KubernetesBearerToken 63 | 64 | restConfig := &rest.Config{ 65 | Host: config.App.KubernetesServiceHost, 66 | TLSClientConfig: rest.TLSClientConfig{ 67 | CAData: caData, 68 | CertData: certData, 69 | KeyData: keyData, 70 | }, 71 | BearerToken: bearerToken, 72 | } 73 | k8sClient, err := kubernetes.NewForConfig(restConfig) 74 | if err != nil { 75 | return nil, errors.Wrap(err, "new kubernetes client") 76 | } 77 | 78 | return &KubernetesTask{ 79 | config: restConfig, 80 | k8sClient: k8sClient, 81 | runner: runner, 82 | 83 | language: language, 84 | template: template, 85 | code: code, 86 | }, nil 87 | } 88 | 89 | func (t *KubernetesTask) Run(ctx context.Context) (*ExecOutput, error) { 90 | namespace := config.App.KubernetesNamespace 91 | name := "elaina-" + uuid.NewV4().String() 92 | falseVal := false 93 | 94 | pod, err := t.k8sClient.CoreV1().Pods(namespace).Create(ctx, &v1.Pod{ 95 | ObjectMeta: metav1.ObjectMeta{ 96 | Name: name, 97 | Namespace: namespace, 98 | Labels: map[string]string{ 99 | "elaina_language": t.language, 100 | }, 101 | }, 102 | Spec: v1.PodSpec{ 103 | Containers: []v1.Container{ 104 | { 105 | Name: name, 106 | Image: t.runner.Image, 107 | ImagePullPolicy: v1.PullIfNotPresent, 108 | SecurityContext: &v1.SecurityContext{ 109 | Privileged: &falseVal, 110 | AllowPrivilegeEscalation: &falseVal, 111 | }, 112 | Resources: v1.ResourceRequirements{ 113 | Limits: v1.ResourceList{ 114 | v1.ResourceCPU: *resource.NewQuantity(t.template.MaxCPUs, resource.DecimalSI), 115 | v1.ResourceMemory: *resource.NewQuantity(t.template.MaxMemory*1024*1024, resource.DecimalSI), 116 | }, 117 | Requests: v1.ResourceList{ 118 | v1.ResourceCPU: resource.MustParse("1m"), 119 | v1.ResourceMemory: *resource.NewQuantity(5*1024*1024, resource.DecimalSI), 120 | }, 121 | }, 122 | Stdin: true, 123 | StdinOnce: true, 124 | TTY: false, 125 | Env: nil, // TODO 126 | }, 127 | }, 128 | AutomountServiceAccountToken: &falseVal, 129 | EnableServiceLinks: &falseVal, 130 | RestartPolicy: v1.RestartPolicyNever, 131 | }, 132 | }, metav1.CreateOptions{}) 133 | if err != nil { 134 | return nil, errors.Wrap(err, "create pod") 135 | } 136 | 137 | defer func() { 138 | zeroVal := int64(0) 139 | 140 | if err := t.k8sClient.CoreV1().Pods(pod.Namespace).Delete(ctx, pod.Name, metav1.DeleteOptions{ 141 | GracePeriodSeconds: &zeroVal, // Delete 142 | }); err != nil { 143 | logrus.WithError(err).Error("Failed to delete pod") 144 | } 145 | }() 146 | 147 | // Wait the pod started. 148 | if err := waitForPodRunning(ctx, t.k8sClient, pod.Namespace, pod.Name, 60*time.Second); err != nil { 149 | return nil, errors.Wrap(err, "wait for pod running") 150 | } 151 | 152 | output, err := t.exec(ctx, pod.Namespace, pod.Name, name, string(t.code)) 153 | if err != nil { 154 | return nil, errors.Wrap(err, "exec") 155 | } 156 | return output, nil 157 | } 158 | 159 | func (t *KubernetesTask) exec(ctx context.Context, namespace, podName, containerName string, code string) (*ExecOutput, error) { 160 | req := t.k8sClient.CoreV1().RESTClient().Post().Resource("pods"). 161 | Namespace(namespace).Name(podName).SubResource("attach"). 162 | VersionedParams(&v1.PodAttachOptions{ 163 | Container: containerName, 164 | Stdin: true, 165 | Stdout: true, 166 | Stderr: true, 167 | TTY: false, 168 | }, scheme.ParameterCodec) 169 | 170 | exec, err := remotecommand.NewSPDYExecutor(t.config, http.MethodPost, req.URL()) 171 | if err != nil { 172 | return nil, errors.Wrap(err, "new executor") 173 | } 174 | 175 | buildCommands := t.runner.BuildCommands 176 | if buildCommands == nil { 177 | buildCommands = []string{} 178 | } 179 | input := ExecInput{ 180 | RunInstructions: ExecInputRunInstructions{ 181 | BuildCommands: buildCommands, 182 | RunCommand: t.runner.RunCommand, 183 | }, 184 | Files: []*ExecInputFile{ 185 | { 186 | Name: t.runner.FileName, 187 | Content: code, 188 | }, 189 | }, 190 | Stdin: nil, 191 | } 192 | stdin, err := json.Marshal(input) 193 | if err != nil { 194 | return nil, errors.Wrap(err, "marshal input") 195 | } 196 | 197 | var stdout, stderr bytes.Buffer 198 | 199 | ctx, cancel := context.WithTimeout(ctx, time.Duration(t.template.Timeout)*time.Second) 200 | defer cancel() 201 | 202 | if err := exec.StreamWithContext(ctx, remotecommand.StreamOptions{ 203 | Stdin: bytes.NewBuffer(stdin), 204 | Stdout: &stdout, 205 | Stderr: &stderr, 206 | Tty: false, 207 | }); err != nil { 208 | return nil, errors.Wrap(err, "stream") 209 | } 210 | 211 | var execOutput ExecOutput 212 | if err := json.NewDecoder(&stdout).Decode(&execOutput); err != nil { 213 | return nil, errors.Wrap(err, "unmarshal output") 214 | } 215 | return &execOutput, nil 216 | } 217 | 218 | // waitForPodRunning polls up to timeout seconds for pod to enter running state. 219 | // Returns an error if the pod never enters the running state. 220 | func waitForPodRunning(ctx context.Context, client kubernetes.Interface, namespace, podName string, timeout time.Duration) error { 221 | return wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, timeout, true, isPodRunning(client, podName, namespace)) 222 | } 223 | 224 | // isPodRunning returns a condition function that indicates whether the given pod is currently running. 225 | func isPodRunning(client kubernetes.Interface, podName, namespace string) wait.ConditionWithContextFunc { 226 | return func(ctx context.Context) (bool, error) { 227 | pod, err := client.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) 228 | if err != nil { 229 | return false, err 230 | } 231 | 232 | switch pod.Status.Phase { 233 | case v1.PodRunning: 234 | return true, nil 235 | case v1.PodFailed, v1.PodSucceeded: 236 | return false, errors.New("pod is finished") 237 | } 238 | return false, nil 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /internal/runtime/runtime.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package runtime 6 | 7 | import ( 8 | "context" 9 | ) 10 | 11 | type ExecInputRunInstructions struct { 12 | BuildCommands []string `json:"buildCommands"` 13 | RunCommand string `json:"runCommand"` 14 | } 15 | 16 | type ExecInputFile struct { 17 | Name string `json:"name"` 18 | Content string `json:"content"` 19 | } 20 | 21 | type ExecInput struct { 22 | RunInstructions ExecInputRunInstructions `json:"runInstructions"` 23 | Files []*ExecInputFile `json:"files"` 24 | Stdin *string `json:"stdin"` 25 | } 26 | 27 | // ExecOutput is the glot output style. 28 | type ExecOutput struct { 29 | Stdout string `json:"stdout"` 30 | Stderr string `json:"stderr"` 31 | Error string `json:"error"` 32 | Duration int64 `json:"duration"` 33 | } 34 | 35 | type ExecRuntime interface { 36 | Run(ctx context.Context) (*ExecOutput, error) 37 | } 38 | -------------------------------------------------------------------------------- /public/css/sandbox.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background-color: #3f414b; 4 | } 5 | 6 | #app { 7 | width: 100%; 8 | } 9 | 10 | .CodeMirror { 11 | font-family: "Source Code Pro", Consolas, monaco, monospace; 12 | letter-spacing: 0.025em; 13 | line-height: 1.25; 14 | font-size: 12px; 15 | height: 50%; 16 | width: 100%; 17 | overflow-y: hidden !important; 18 | -webkit-overflow-scrolling: touch; 19 | } 20 | 21 | .header { 22 | display: flex; 23 | position: relative; 24 | height: 50px; 25 | padding-left: 20px; 26 | padding-right: 20px; 27 | width: 100%; 28 | } 29 | 30 | .title { 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | box-sizing: border-box; 35 | 36 | color: #cccccc; 37 | font-size: 15px; 38 | font-weight: bolder; 39 | vertical-align: middle; 40 | } 41 | 42 | .lang { 43 | display: flex; 44 | justify-content: center; 45 | align-items: center; 46 | box-sizing: border-box; 47 | } 48 | 49 | .label { 50 | margin-right: 0; 51 | display: inline-block; 52 | padding: 0 10px; 53 | background: #4e6173; 54 | line-height: 1.5; 55 | font-size: 12px; 56 | color: #fff; 57 | vertical-align: middle; 58 | white-space: nowrap; 59 | border-radius: 2px; 60 | } 61 | 62 | 63 | .result { 64 | height: calc(50% - 65px); 65 | font-family: monospace; 66 | background-color: #1a1d28; 67 | color: #fff; 68 | padding: 0 20px 0 20px; 69 | overflow-y: scroll; 70 | word-break: normal; 71 | white-space: pre-line; 72 | } 73 | 74 | .footer { 75 | position: absolute; 76 | width: 100%; 77 | height: 30px; 78 | bottom: 0; 79 | text-align: right; 80 | padding: 5px; 81 | font-size: 12px; 82 | color: #ffffff50; 83 | background-color: #3f414b; 84 | } 85 | 86 | details summary { 87 | -webkit-touch-callout: none; /* iOS Safari */ 88 | -webkit-user-select: none; /* Chrome/Safari/Opera */ 89 | -khtml-user-select: none; /* Konqueror */ 90 | -moz-user-select: none; /* Firefox */ 91 | -ms-user-select: none; /* Internet Explorer/Edge */ 92 | user-select: none; 93 | /* Non-prefixed version, currently 94 | not supported by any browser */ 95 | } 96 | 97 | details summary:focus { 98 | outline: none; 99 | } 100 | 101 | details summary.single-line { 102 | cursor: default; 103 | } 104 | 105 | details summary.single-line + p { 106 | display: none; 107 | } 108 | 109 | /*Chrome*/ 110 | details summary.single-line::-webkit-details-marker { 111 | visibility: hidden; 112 | } 113 | 114 | /*Firefox*/ 115 | details summary.single-line:first-of-type { 116 | list-style: none inside url(data:image/gif;base64,R0lGODlhCgABAIAAAP///wAAACH5BAEAAAAALAAAAAAKAAEAAAIDhI8FADs=); 117 | } -------------------------------------------------------------------------------- /public/fs.go: -------------------------------------------------------------------------------- 1 | package public 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed css/* js/* 8 | var FS embed.FS 9 | -------------------------------------------------------------------------------- /public/js/sandbox.js: -------------------------------------------------------------------------------- 1 | // Setted by iframe. 2 | let languagePlaceholder = []; 3 | 4 | // Codemirror editor. 5 | var editor = CodeMirror.fromTextArea(document.getElementById('code'), { 6 | lineNumbers: true, 7 | mode: ((lang) => { 8 | if (lang === 'c') { 9 | return 'text/x-csrc' 10 | } 11 | return lang 12 | })(lang), 13 | theme: 'material-palenight' 14 | }); 15 | 16 | $('#run').click(() => { 17 | // Reset the text color. 18 | $('#result_bar').css('color', 'white'); 19 | 20 | $('#result_data').text('Loading...'); 21 | 22 | $.post(window.location.href + '/execute', {'lang': lang, 'code': editor.getValue()}, (res) => { 23 | $('#result_data').text(''); 24 | const {error, stderr, stdout} = res.data.result; 25 | 26 | if (error) { 27 | const build_details = $('
'); 28 | const build_summary = $('').text('Error'); 29 | build_details.append(build_summary); 30 | build_details.append($('

').text(error)); 31 | 32 | $('#result_bar').css('color', 'red'); 33 | $('#result_data').append(build_details); 34 | $('#result_data').append($('

').text(stderr)); 35 | 36 | } else { 37 | $('#result_bar').css('color', 'white'); 38 | $('#result_data').text(stdout); 39 | } 40 | 41 | let startAt = res.data.start_at; 42 | let endAt = res.data.end_at; 43 | $('#time_cost').text(((endAt - startAt) / 1000000000) + 's'); 44 | }).fail((err) => { 45 | $('#result_bar').css('color', 'red'); 46 | $('#result_data').text(err.responseJSON.msg); 47 | }) 48 | }) 49 | 50 | // Switch language 51 | $('[lang]').click((evt) => { 52 | // Save the current code into language placeholder. 53 | languagePlaceholder[lang] = editor.getValue(); 54 | 55 | // Set the new language. 56 | lang = evt.target.lang; 57 | $('#lang').text(lang); 58 | editor.setOption('mode', lang); 59 | 60 | // Recover the language placeholder. 61 | if (languagePlaceholder[lang] === undefined) { 62 | languagePlaceholder[lang] = editor.getValue(); 63 | } 64 | editor.setValue(languagePlaceholder[lang]); 65 | }) 66 | 67 | // Receive the code from the outside iframe. 68 | window.addEventListener('message', (evt) => { 69 | if (evt.data.type === 'elaina') { 70 | languagePlaceholder = evt.data.language 71 | let code = Base64.decode(evt.data.code ?? ''); 72 | if (code !== '') { 73 | editor.setValue(code); 74 | $('#post-code').val(code) 75 | } 76 | } 77 | }, false); 78 | 79 | // Show button in iframe. 80 | if (window.self !== window.top) { 81 | $('#new-window').show() 82 | } 83 | -------------------------------------------------------------------------------- /templates/fs.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed *.tmpl 8 | var FS embed.FS 9 | -------------------------------------------------------------------------------- /templates/sandbox.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ .Sandbox.Name }} - Elaina 4 | 5 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
{{ .Sandbox.Name }}
15 |
16 |
17 |
{{ .Language }}
18 |
19 | {{ if ne (len .Languages) 1 }} 20 |
21 |
22 | {{ range $index, $lang := .Languages }} 23 | 24 | {{ end }} 25 |
26 |
27 | {{ end }} 28 |
29 | 30 |
31 | 32 | 35 |
36 |
37 |
38 | 39 | 40 | 41 |
42 | 43 |
44 | 45 | 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .vscode 26 | -------------------------------------------------------------------------------- /web/fs.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed dist 8 | var FS embed.FS 9 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Elaina 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elaina-web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "axios": "^1.7.2", 13 | "dayjs": "^1.11.11", 14 | "less": "^4.2.0", 15 | "pinia": "^2.1.7", 16 | "pinia-plugin-persistedstate": "^3.2.1", 17 | "sass": "^1.77.2", 18 | "tdesign-vue-next": "^1.9.5", 19 | "vite-svg-loader": "^5.1.0", 20 | "vue": "^3.4.21", 21 | "vue-router": "4" 22 | }, 23 | "devDependencies": { 24 | "@vitejs/plugin-vue": "^5.0.4", 25 | "typescript": "^5.2.2", 26 | "vite": "^5.2.0", 27 | "vue-tsc": "^2.0.6" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /web/src/api/auth.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export interface SignInForm { 4 | password: string; 5 | } 6 | 7 | export function signIn(data: SignInForm) { 8 | return axios.post('/api/auth/sign-in', data) 9 | } 10 | 11 | export interface ProfileResp { 12 | 13 | } 14 | 15 | export function profile() { 16 | return axios.get('/api/auth/profile') 17 | } 18 | -------------------------------------------------------------------------------- /web/src/api/interceptor.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import type {AxiosResponse} from 'axios'; 3 | import {MessagePlugin} from 'tdesign-vue-next'; 4 | import {useAuthStore} from '@/store'; 5 | 6 | export interface HttpResponse { 7 | msg: string; 8 | data: T; 9 | } 10 | 11 | axios.interceptors.request.use( 12 | (config) => { 13 | const authStore = useAuthStore(); 14 | const token = authStore.token 15 | if (authStore.isAuthenticated && token) { 16 | config.headers.Authorization = `Token ${token}`; 17 | } 18 | return config; 19 | }, 20 | (error) => { 21 | return Promise.reject(error); 22 | } 23 | ); 24 | 25 | axios.interceptors.response.use( 26 | (response: AxiosResponse) => { 27 | const res = response.data; 28 | const statusCode = response.status 29 | if (statusCode / 100 !== 2) { 30 | MessagePlugin.error(res.msg || 'Unknown error') 31 | } 32 | return res.data; 33 | }, 34 | (error) => { 35 | const statusCode = error.response.status 36 | if (statusCode === 401) { 37 | const authStore = useAuthStore(); 38 | authStore.cleanToken(); 39 | window.location.reload(); 40 | return 41 | } 42 | 43 | const msg = error.response.data.msg 44 | MessagePlugin.error(msg || 'Unknown error') 45 | return Promise.reject(error); 46 | } 47 | ); 48 | -------------------------------------------------------------------------------- /web/src/api/sandbox.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import {Template} from "@/api/template"; 3 | 4 | export interface Sandbox { 5 | id: number; 6 | createdAt: Date; 7 | updatedAt: Date; 8 | 9 | uid: string; 10 | name: string; 11 | templateID: number; 12 | template: Template; 13 | placeholder: string; 14 | editable: boolean; 15 | } 16 | 17 | export interface ListSandboxesResp { 18 | sandboxes: Sandbox[]; 19 | total: number; 20 | } 21 | 22 | export function listSandboxes(params: { page: number; pageSize: number }) { 23 | return axios.get('/api/sandboxes', {params}); 24 | } 25 | 26 | export interface CreateSandboxReq { 27 | name: string; 28 | templateID: number; 29 | placeholder: string; 30 | editable: boolean; 31 | } 32 | 33 | export function createSandbox(data: CreateSandboxReq) { 34 | return axios.post('/api/sandboxes', data); 35 | } 36 | 37 | export function getSandbox(id: string) { 38 | return axios.get(`/api/sandbox/${id}`); 39 | } 40 | 41 | export interface UpdateSandboxReq { 42 | name: string; 43 | templateID: number; 44 | placeholder: string; 45 | editable: boolean; 46 | } 47 | 48 | export function updateSandbox(id: string, data: UpdateSandboxReq) { 49 | return axios.put(`/api/sandbox/${id}`, data); 50 | } 51 | 52 | export function deleteSandbox(id: number) { 53 | return axios.delete(`/api/sandbox/${id}`); 54 | } 55 | -------------------------------------------------------------------------------- /web/src/api/template.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export interface Template { 4 | id: number; 5 | createdAt: Date; 6 | updatedAt: Date; 7 | 8 | name: string; 9 | language: string[]; 10 | timeout: number; 11 | maxCpus: number; 12 | maxMemory: number; 13 | internetAccess: boolean; 14 | dns: { [key: string]: string }; 15 | maxContainer: number; 16 | maxContainerPerIp: number; 17 | } 18 | 19 | export interface ListTemplatesResp { 20 | templates: Template[]; 21 | total: number; 22 | } 23 | 24 | export function listTemplates(params: { page: number; pageSize: number }) { 25 | return axios.get('/api/templates', {params}); 26 | } 27 | 28 | export function allTemplates() { 29 | return axios.get('/api/templates/all'); 30 | } 31 | 32 | export interface CreateTemplateReq { 33 | name: string; 34 | language: string[]; 35 | timeout: number; 36 | maxCpus: number; 37 | maxMemory: number; 38 | internetAccess: boolean; 39 | dns: { [key: string]: string }; 40 | maxContainer: number; 41 | maxContainerPerIp: number; 42 | } 43 | 44 | export function createTemplate(data: CreateTemplateReq) { 45 | return axios.post('/api/templates', data); 46 | } 47 | 48 | export function getTemplate(id: string) { 49 | return axios.get(`/api/template/${id}`); 50 | } 51 | 52 | export interface UpdateTemplateReq { 53 | name: string; 54 | language: string[]; 55 | timeout: number; 56 | maxCpus: number; 57 | maxMemory: number; 58 | internetAccess: boolean; 59 | dns: { [key: string]: string }; 60 | maxContainer: number; 61 | maxContainerPerIp: number; 62 | } 63 | 64 | export function updateTemplate(id: string, data: UpdateTemplateReq) { 65 | return axios.put(`/api/template/${id}`, data); 66 | } 67 | 68 | export function deleteTemplate(id: number) { 69 | return axios.delete(`/api/template/${id}`); 70 | } 71 | -------------------------------------------------------------------------------- /web/src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 62 | 63 | 68 | -------------------------------------------------------------------------------- /web/src/const/template.ts: -------------------------------------------------------------------------------- 1 | export const LANGUAGES = [ 2 | {label: 'PHP', value: 'php'}, 3 | {label: 'Python', value: 'python'}, 4 | {label: 'Go', value: 'go'}, 5 | {label: 'JavaScript', value: 'javascript'}, 6 | {label: 'C', value: 'c'}, 7 | ] 8 | 9 | export const LANGUAGES_MAP: { [key: string]: string } = { 10 | 'php': 'PHP', 11 | 'python': 'Python', 12 | 'go': 'Go', 13 | 'javascript': 'JavaScript', 14 | 'c': 'C', 15 | } 16 | -------------------------------------------------------------------------------- /web/src/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue' 2 | 3 | import App from './App.vue' 4 | import TDesign from 'tdesign-vue-next'; 5 | import './theme.css' 6 | import 'tdesign-vue-next/es/style/index.css'; 7 | import store from './store' 8 | import router from './route/index' 9 | import './api/interceptor' 10 | import '@/style/index.less' 11 | 12 | const app = createApp(App) 13 | app.use(TDesign) 14 | app.use(store) 15 | app.use(router) 16 | app.mount('#app') 17 | -------------------------------------------------------------------------------- /web/src/route/index.ts: -------------------------------------------------------------------------------- 1 | import {createRouter, createWebHistory, RouteRecordRaw} from "vue-router"; 2 | import {useAuthStore} from "@/store"; 3 | 4 | const allRouters: Array = [ 5 | { 6 | path: '/sign-in', 7 | name: 'signIn', 8 | component: () => import('@/views/SignIn.vue') 9 | }, 10 | { 11 | path: '', 12 | redirect: '/dashboard' 13 | }, 14 | { 15 | path: '', 16 | name: 'layout', 17 | component: () => import('@/views/Layout.vue'), 18 | children: [ 19 | { 20 | path: '/dashboard', 21 | name: 'dashboard', 22 | component: () => import('@/views/Dashboard.vue') 23 | }, 24 | { 25 | path: '/template', 26 | name: 'template', 27 | component: () => import('@/views/Template.vue') 28 | }, 29 | { 30 | path: '/template/new', 31 | name: 'createTemplate', 32 | component: () => import('@/views/TemplateModify.vue') 33 | }, 34 | { 35 | path: '/template/:id', 36 | name: 'editTemplate', 37 | component: () => import('@/views/TemplateModify.vue') 38 | }, 39 | { 40 | path: '/sandbox', 41 | name: 'sandbox', 42 | component: () => import('@/views/Sandbox.vue') 43 | }, 44 | { 45 | path: '/sandbox/new', 46 | name: 'createSandbox', 47 | component: () => import('@/views/SandboxModify.vue') 48 | }, 49 | { 50 | path: '/sandbox/:id', 51 | name: 'editSandbox', 52 | component: () => import('@/views/SandboxModify.vue') 53 | } 54 | ] 55 | } 56 | ] 57 | 58 | const router = createRouter({ 59 | history: createWebHistory(), 60 | routes: allRouters, 61 | scrollBehavior() { 62 | return {el: '#app', top: 0, behavior: 'smooth'} 63 | } 64 | }) 65 | 66 | router.beforeEach((to, from, next) => { 67 | const authStore = useAuthStore() 68 | 69 | if (to.name !== 'signIn' && !authStore.isAuthenticated) { 70 | next({name: 'signIn'}) 71 | } else { 72 | next() 73 | } 74 | }) 75 | 76 | export default router; 77 | -------------------------------------------------------------------------------- /web/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import {createPinia} from 'pinia'; 2 | import useAuthStore from './modules/auth'; 3 | 4 | const pinia = createPinia(); 5 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' 6 | 7 | pinia.use(piniaPluginPersistedstate) 8 | 9 | export {useAuthStore}; 10 | export default pinia; 11 | -------------------------------------------------------------------------------- /web/src/store/modules/auth/index.ts: -------------------------------------------------------------------------------- 1 | import {defineStore} from "pinia"; 2 | import {AuthState} from "./types"; 3 | 4 | const useAuthStore = defineStore('auth', { 5 | persist: true, 6 | 7 | state: (): AuthState => ({ 8 | isAuthenticated: false, 9 | token: '', 10 | }), 11 | 12 | actions: { 13 | setToken(token: string) { 14 | this.token = token; 15 | this.isAuthenticated = true; 16 | }, 17 | 18 | cleanToken() { 19 | this.token = ''; 20 | this.isAuthenticated = false; 21 | }, 22 | } 23 | }) 24 | 25 | export default useAuthStore; 26 | -------------------------------------------------------------------------------- /web/src/store/modules/auth/types.ts: -------------------------------------------------------------------------------- 1 | export interface AuthState { 2 | isAuthenticated: boolean; 3 | token: string; 4 | } 5 | -------------------------------------------------------------------------------- /web/src/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuhan005/Elaina/6feaff8515b1793767539df7034f24fda97f2dbc/web/src/style.css -------------------------------------------------------------------------------- /web/src/style/font-family.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'TencentSansW7'; 3 | src: url('data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAusAA4AAAAAEJQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAALkAAAABwAAAAchqPqzUdERUYAAAtwAAAAHgAAAB4AKQAbT1MvMgAAAbgAAABZAAAAYGmceoNjbWFwAAACYAAAAJcAAAHsPmfPZmdhc3AAAAtkAAAADAAAAAwACAAbZ2x5ZgAAAywAAAW8AAAG/Ivn/ztoZWFkAAABRAAAADYAAAA2E+AL5GhoZWEAAAF8AAAAIAAAACQIawJ9aG10eAAAAhQAAABMAAAATCG/Auxsb2NhAAADAAAAACwAAAAsDjIQIm1heHAAAAGcAAAAGgAAACAAfgBDbmFtZQAACOgAAAIUAAAEm0zGvtJwb3N0AAAK/AAAAGYAAAB/4wuGdnByZXAAAAL4AAAACAAAAAhwAgESAAEAAAABBR/xlpGAXw889QALA+gAAAAA2Ac3gwAAAADY+IxB//L/HAPPAwAAAAAIAAIAAAAAAAB42mNgZGBgWf7vFAMD84v/n/7vZD7PABRBAYIAwxQH7XjaY2BkYGAQZXBiYGEAAUYGGEiBUAAMEQDCAAB42mNgYepm2sPAysDA1MUUwcDA4A2hGeMYjBjNgKI8HMxMTCz8TCwLGJj2CzCAgRiI8PX382d0YGBMEmQ2+u/FcIJlOVA9CwMjSI6JlekwkFJgYAQAR1kL+QAAAAJYAHYAAAAAAU0AAAEEAAACUAAhAlYAFQJUACACKgAdAZUANgEUABUBYAAkA5wAFQINABsBqAA0AnAAKgJYACoD6ACF//YANP/yACN42mNgYGBmgGAZBkYGEHgG5DGC+SwMp4C0HIMAUISPQYEhiSGNIZMhl6GUoZJhgeIkfS6/N4GpQQuSBP//B+tMZEgByucwFGOT/7/4/6L/C/7P+z/z/7T/yffqLrJvVFu3Zm3xPJBtcgz4ADPFkIGRDWgMIcBAIWBhZWBj52Bg4GRg4OIGi/Dw8gFJfgYqA/JcCgA99Se8ALgB/4W4AAGNAAAAFAAUABQAFABSAIIAsgD6ASIBOAFYAYIBxgHwAhQCRAJaAogCygMYA3542k1Ua2xTZRj+LmtP23Vdz2lPz3pZb2dr1+u2nq2H0d3Z2OhI5mC4AZMBo0gM98E0oRn1AqgoIYDG4BAkakDkJ4iyiCZGAiISUH8YjIQfEhNUMCoJrme+bTfkx/nxveec53ne533eDxE0Nn0V/0V2I4oYhATWw1ZKrDiGx5Vfzp6NkXPZ7mH8ECGCPNNXiZWokANVIYRjFt7MUI83iuvrWnAzTWA5Xl/nC2G/SZJFr7oUq3mzBaf7F5S0Kt+F59i1aq0j0tbwJmXcwUtsz3HHhEtQaYr0RFUbL0sqB8yRClvcG2uwa7hKg4VLKdFjZmeEN5SwwM0Dd4DcRaXAXuAuxQYqekwSIxRoqRRrxjLevrK/RNCUhXt7lYevpQMf6StjW1sz/gCrIqqm+Z7krsjCmJU67/zexrvMykE+CniA7wb8oUf4EhW9vtm2qGQSvQY808+t9Ov/5ih0zlhfyytfbGodd/ht2rJiH7k7dTtP0NQTYpUo3qDzSnsSVpc5j18O+DzRIzeqyel34nwDOISZHGoCM6I3SvKMRJppKM8pxeIy3tbU4S4y6Oc231Sp3OFLgXMV7dV2xq8L9K9IUZvY5XAZi4wfms2U6K21PoHvnVfvchitXEorhH2O4K253RUTnIYt0Xv4ITanB6M4zPwm9GuBQ4GIEaloEmVJzvXM4JeqXaxWfYg7tCi9qIddqNZZg53yKEv2lLgkEWaDr6fERHfEnEL5/mA+uJzcLmTIJEsMNUl0t5AR7o+kR8jqTCa7nsjZS3nuCvg2DtwcpC3ftQzfgg1ODMRfBjt8eiP8ZLAE7HNDsRJPUKhYTmqy13osvJGm4H/gomsgh8Ksk2oGpM+kAHqAh0wmuitpSWO6SWer2NOX7vu8D2SouFCACSSVk3hgjmBllRp8Takhck6THTT9DZjVoMkPprfgmSE8igHzKNOJ//MX4X4rN6m0VF022lan02g4R11DNngDahqoPQe1YoZ31SRiKWN5fammqNgddMiRcNheAoXIbKGlKsoXo7w3daDjFDmNbOAiM+uJOONSbjoSIzEPI1owpyO0oL6twlAcSbI9zrTzKIePTdeaeZamOHe4zJtA06kTJ3J+BSF7vdCbKZ/swsbGZzyHZvCN9BrlzmN+vUVU2UsFq/CAcjJnVV5bcNpB2gCnPI9jILDquZ1rwflleQzwqx1ri8PJzh+4K3GnUcPsg32pSdfAJmYA+dPm+a61rBj0N3Z5ktio3Gu1uAqZ3IVW0R2Ar4GDwIiyp97jJ5MXLuzbn71IGvYT1fXrR545kvdp+p/pNB0nvyI7QiqR+A2UEZupLPmjOK/KIlg4On741U6xf3hj49Ha0e2bov3tVFd6oOx2uVvb9dlLeOTjxt1797XUvDBx+snTOv1Sk/2y0g7YRTCDNP2WIqSHhLqQCHQGnEuYmpHhhvFYBNHnV8UsUPJLpjjwYZNA8ZC+yv7Uxu5QiG/vMWOfNzzq9uH7XqFoyz3byRWUv1ClnNowsoqi+CYp248fhN0TC94YXrSkYyRod03dWkYOmpdOfUAetGWTiE7/CRomQYML+VG0kHOG5AjjpjjE0kAYMc7JubMsqZnK3GvqMQmNOHcR4u9t5Woai31t3xXxYVrcaZQXt+5cOWZZV7ZXp94Io6u1C5Q+rZzY+pP62YEdZEmpx6QcUb4ZnMJ2nXbs2uWtb+P57w6sSoYytQYnl62juhVEnT1e2HURtF0EbQnUBsrAD9AADglwhfgLHkWxX2TiMqPOeyQzftinXHjhbVwuJVIdiOY9fvBRstDzyh9Ys6B6W2h0HnvO85DbvMsa2xJhNPgiPzBkaN4W/NF+0PaOd/ug7Yz+DNfVp3/v5+Jx8yRdT3F5dLMorLXik6PrnhhYfKAxM/hyb0Oanycm3+86bHMPC6JyZfB8YJnN8sngi4xqqdq3nN0//vzOTNXq5YsR+g8984WfeNq1Us1qFEEQ/npnk0X8IQGJIjnUSRLYLLt7MMlFCHvNKRvMuTPTmZ1kdib0zAY3ePMFfAAvigi5+Ry+gA8iiOLFr3tbTFZWcnGgu76qrvqqpqoAPMQ3KMy+A3wMWOGBehRwAy31LOAIayoPuEmfdwEv4Z76HPAy7quvAbfwqvEz4BXcjd4EvIpG9J5sqnmH2gfP7LDCOn4E3CD/04AjtNXzgJtYV68DXsJj9SngZTxRXwJu4XtDBbyCtehlwKtoRm8xQIlzTGGRIcUINQRXPH100cMOtgLape0QBgVif9dBjxlTkCPnSckj2MCQNudzGeQmrS5PB22ifcYmxII9RuWUf3JXXjOUhvKCt/PEoDyf2iwd1XIl/W5vZ4vXrhyaIjZFTRmPijIv06lsDEemuOTZlEHZact+nXRkL8/FR1diTWXshUnIefNnhtCULv0Rtvk4ox7qopKjbbcNhhVOWK1mXTgw6STX9t8kMh91k1RuRfJXJS98Zyp2rKSbcDIdzqfPB2OrrCyk1+n2F3HOMzrC+aFmPrcOg0i9XvukbhhCbPmaUBv73zqjrcTJf1gPV7PL6PK4yGN6L6oq882IvaWm/0w/ZfOt9014x3yZta1yS/V7fbJKNBcjzaraWJNIbXVixtqeSXly6x3TRSJjPZVjc50qKyQ2ttaUpxObVUkW15xRtXD9rg8Hs3FxRr8ATJnl93jaY2BiAIP/zQxGDNiAKBAzMjAxMjG4MLgyuDN4MHgy+DD4MwQwhDGEM0QwxDAyM7IwsjKyMbKzl+ZlGhgYGHIlFhXllxdlpmeUgISM3AwcQbSJq6sziDY1cjQA0WZGhoYAgBwU3AAAAAEAAgAIAAr//wAPAAEAAAAMAAAAFgAAAAIAAQADABQAAQAEAAAAAgAAAAAAAAABAAAAANWkJwgAAAAA2Ac3gwAAAADY+IxB') 4 | format('woff'); 5 | font-weight: normal; 6 | font-style: normal; 7 | } 8 | -------------------------------------------------------------------------------- /web/src/style/form.less: -------------------------------------------------------------------------------- 1 | .form-basic-container { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | background-color: var(--td-bg-color-container); 6 | border-radius: var(--td-radius-medium) var(--td-radius-medium) 0 0; 7 | padding: var(--td-comp-paddingTB-xxl) var(--td-comp-paddingLR-xxl) 80px var(--td-comp-paddingLR-xxl); 8 | 9 | .form-basic-item { 10 | width: 676px; 11 | 12 | .form-basic-container-title { 13 | font: var(--td-font-title-large); 14 | font-weight: 400; 15 | color: var(--td-text-color-primary); 16 | margin: var(--td-comp-margin-xxl) 0 var(--td-comp-margin-xl) 0; 17 | } 18 | 19 | .form-title-gap { 20 | margin: calc(var(--td-comp-margin-xxl) * 2) 0 var(--td-comp-margin-xl) 0; 21 | } 22 | } 23 | } 24 | 25 | .form-submit-container { 26 | width: 100%; 27 | display: flex; 28 | align-items: center; 29 | justify-content: center; 30 | padding-top: var(--td-comp-paddingLR-xl); 31 | padding-bottom: var(--td-comp-paddingLR-xl); 32 | background-color: var(--td-bg-color-secondarycontainer); 33 | border-bottom-left-radius: var(--td-radius-medium); 34 | border-bottom-right-radius: var(--td-radius-medium); 35 | border-top: 1px solid var(--td-component-stroke); 36 | 37 | .form-submit-sub { 38 | width: 676px; 39 | display: flex; 40 | align-items: center; 41 | justify-content: space-between; 42 | 43 | .form-submit-left { 44 | .form-submit-upload-span { 45 | font-size: 14px; 46 | line-height: 22px; 47 | color: var(--td-text-color-placeholder); 48 | padding-top: 8px; 49 | display: inline-block; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /web/src/style/index.less: -------------------------------------------------------------------------------- 1 | @import './font-family.less'; 2 | @import './reset.less'; 3 | -------------------------------------------------------------------------------- /web/src/style/reset.less: -------------------------------------------------------------------------------- 1 | body { 2 | color: var(--td-text-color-secondary); 3 | font: var(--td-font-body-medium); 4 | font-family: -apple-system, BlinkMacSystemFont, var(--td-font-family); 5 | -webkit-font-smoothing: antialiased; 6 | padding: 0; 7 | margin: 0; 8 | } 9 | 10 | pre { 11 | font-family: var(--td-font-family); 12 | } 13 | 14 | ul, 15 | dl, 16 | li, 17 | dd, 18 | dt { 19 | margin: 0; 20 | padding: 0; 21 | list-style: none; 22 | } 23 | 24 | figure, 25 | h1, 26 | h2, 27 | h3, 28 | h4, 29 | h5, 30 | h6, 31 | p { 32 | margin: 0; 33 | } 34 | 35 | * { 36 | box-sizing: border-box; 37 | } 38 | -------------------------------------------------------------------------------- /web/src/style/variables.less: -------------------------------------------------------------------------------- 1 | @screen-sm: 768px; 2 | @screen-md: 992px; 3 | @screen-lg: 1200px; 4 | @screen-xl: 1400px; 5 | 6 | @screen-sm-min: @screen-sm; 7 | @screen-md-min: @screen-md; 8 | @screen-lg-min: @screen-lg; 9 | @screen-xl-min: @screen-xl; 10 | 11 | @screen-sm-max: calc(@screen-md-min - 1px); 12 | @screen-md-max: calc(@screen-lg-min - 1px); 13 | @screen-lg-max: calc(@screen-xl-min - 1px); 14 | 15 | @anim-time-fn-easing: cubic-bezier(0.38, 0, 0.24, 1); 16 | @anim-time-fn-ease-out: cubic-bezier(0, 0, 0.15, 1); 17 | @anim-time-fn-ease-in: cubic-bezier(0.82, 0, 1, 0.9); 18 | @anim-duration-base: 0.2s; 19 | @anim-duration-moderate: 0.24s; 20 | @anim-duration-slow: 0.28s; 21 | -------------------------------------------------------------------------------- /web/src/theme.css: -------------------------------------------------------------------------------- 1 | :root,:root[theme-mode="light"] { 2 | --brand-main: var(--td-brand-color-6); 3 | --td-brand-color-light: var(--td-brand-color-1); 4 | --td-brand-color-focus: var(--td-brand-color-2); 5 | --td-brand-color-disabled: var(--td-brand-color-3); 6 | --td-brand-color-hover: var(--td-brand-color-5); 7 | --td-brand-color: var(--td-brand-color-6); 8 | --td-brand-color-active: var(--td-brand-color-7); 9 | --td-brand-color-1: #f9f1ff; 10 | --td-brand-color-2: #eadeff; 11 | --td-brand-color-3: #d4c2ff; 12 | --td-brand-color-4: #baa0ff; 13 | --td-brand-color-5: #9e7cfd; 14 | --td-brand-color-6: #805edd; 15 | --td-brand-color-7: #633fbe; 16 | --td-brand-color-8: #4920a3; 17 | --td-brand-color-9: #310085; 18 | --td-brand-color-10: #20005e; 19 | --td-warning-color-1: #fef3e6; 20 | --td-warning-color-2: #f9e0c7; 21 | --td-warning-color-3: #f7c797; 22 | --td-warning-color-4: #f2995f; 23 | --td-warning-color-5: #ed7b2f; 24 | --td-warning-color-6: #d35a21; 25 | --td-warning-color-7: #ba431b; 26 | --td-warning-color-8: #9e3610; 27 | --td-warning-color-9: #842b0b; 28 | --td-warning-color-10: #5a1907; 29 | --td-warning-color: var(--td-warning-color-5); 30 | --td-warning-color-hover: var(--td-warning-color-4); 31 | --td-warning-color-focus: var(--td-warning-color-2); 32 | --td-warning-color-active: var(--td-warning-color-6); 33 | --td-warning-color-disabled: var(--td-warning-color-3); 34 | --td-warning-color-light: var(--td-warning-color-1); 35 | --td-error-color-1: #fdecee; 36 | --td-error-color-2: #f9d7d9; 37 | --td-error-color-3: #f8b9be; 38 | --td-error-color-4: #f78d94; 39 | --td-error-color-5: #f36d78; 40 | --td-error-color-6: #e34d59; 41 | --td-error-color-7: #c9353f; 42 | --td-error-color-8: #b11f26; 43 | --td-error-color-9: #951114; 44 | --td-error-color-10: #680506; 45 | --td-error-color: var(--td-error-color-6); 46 | --td-error-color-hover: var(--td-error-color-5); 47 | --td-error-color-focus: var(--td-error-color-2); 48 | --td-error-color-active: var(--td-error-color-7); 49 | --td-error-color-disabled: var(--td-error-color-3); 50 | --td-error-color-light: var(--td-error-color-1); 51 | --td-success-color-1: #e8f8f2; 52 | --td-success-color-2: #bcebdc; 53 | --td-success-color-3: #85dbbe; 54 | --td-success-color-4: #48c79c; 55 | --td-success-color-5: #00a870; 56 | --td-success-color-6: #078d5c; 57 | --td-success-color-7: #067945; 58 | --td-success-color-8: #056334; 59 | --td-success-color-9: #044f2a; 60 | --td-success-color-10: #033017; 61 | --td-success-color: var(--td-success-color-5); 62 | --td-success-color-hover: var(--td-success-color-4); 63 | --td-success-color-focus: var(--td-success-color-2); 64 | --td-success-color-active: var(--td-success-color-6); 65 | --td-success-color-disabled: var(--td-success-color-3); 66 | --td-success-color-light: var(--td-success-color-1); 67 | --td-gray-color-1: #f3f3f3; 68 | --td-gray-color-2: #eee; 69 | --td-gray-color-3: #e7e7e7; 70 | --td-gray-color-4: #dcdcdc; 71 | --td-gray-color-5: #c5c5c5; 72 | --td-gray-color-6: #a6a6a6; 73 | --td-gray-color-7: #8b8b8b; 74 | --td-gray-color-8: #777; 75 | --td-gray-color-9: #5e5e5e; 76 | --td-gray-color-10: #4b4b4b; 77 | --td-gray-color-11: #383838; 78 | --td-gray-color-12: #2c2c2c; 79 | --td-gray-color-13: #242424; 80 | --td-gray-color-14: #181818; 81 | --td-bg-color-container: #fff; 82 | --td-bg-color-container-select: #fff; 83 | --td-bg-color-page: var(--td-gray-color-2); 84 | --td-bg-color-container-hover: var(--td-gray-color-1); 85 | --td-bg-color-container-active: var(--td-gray-color-3); 86 | --td-bg-color-secondarycontainer: var(--td-gray-color-1); 87 | --td-bg-color-secondarycontainer-hover: var(--td-gray-color-2); 88 | --td-bg-color-secondarycontainer-active: var(--td-gray-color-4); 89 | --td-bg-color-component: var(--td-gray-color-3); 90 | --td-bg-color-component-hover: var(--td-gray-color-4); 91 | --td-bg-color-component-active: var(--td-gray-color-6); 92 | --td-bg-color-component-disabled: var(--td-gray-color-2); 93 | --td-component-stroke: var(--td-gray-color-3); 94 | --td-component-border: var(--td-gray-color-4); 95 | --td-font-white-1: #ffffff; 96 | --td-font-white-2: rgba(255, 255, 255, 0.55); 97 | --td-font-white-3: rgba(255, 255, 255, 0.35); 98 | --td-font-white-4: rgba(255, 255, 255, 0.22); 99 | --td-font-gray-1: rgba(0, 0, 0, 0.9); 100 | --td-font-gray-2: rgba(0, 0, 0, 0.6); 101 | --td-font-gray-3: rgba(0, 0, 0, 0.4); 102 | --td-font-gray-4: rgba(0, 0, 0, 0.26); 103 | --td-brand-color-light-hover: var(--td-brand-color-2); 104 | --td-warning-color-light-hover: var(--td-warning-color-2); 105 | --td-error-color-light-hover: var(--td-error-color-2); 106 | --td-success-color-light-hover: var(--td-success-color-2); 107 | --td-bg-color-secondarycomponent: var(--td-gray-color-4); 108 | --td-bg-color-secondarycomponent-hover: var(--td-gray-color-5); 109 | --td-bg-color-secondarycomponent-active: var(--td-gray-color-6); 110 | --td-table-shadow-color: rgba(0, 0, 0, 8%); 111 | --td-scrollbar-color: rgba(0, 0, 0, 10%); 112 | --td-scrollbar-hover-color: rgba(0, 0, 0, 30%); 113 | --td-scroll-track-color: #fff; 114 | --td-bg-color-specialcomponent: #fff; 115 | --td-border-level-1-color: var(--td-gray-color-3); 116 | --td-border-level-2-color: var(--td-gray-color-4); 117 | --td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc; 118 | --td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc; 119 | --td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc; 120 | --td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc; 121 | --td-mask-active: rgba(0, 0, 0, 0.6); 122 | --td-mask-disabled: rgba(255, 255, 255, 0.6); 123 | /* 字体配置 */ 124 | --td-font-family: PingFang SC, Microsoft YaHei, Arial Regular; 125 | --td-font-family-medium: PingFang SC, Microsoft YaHei, Arial Medium; 126 | --td-font-size-link-small: 12px; 127 | --td-font-size-link-medium: 14px; 128 | --td-font-size-link-large: 16px; 129 | --td-font-size-mark-small: 12px; 130 | --td-font-size-mark-medium: 14px; 131 | --td-font-size-body-small: 12px; 132 | --td-font-size-body-medium: 14px; 133 | --td-font-size-body-large: 16px; 134 | --td-font-size-title-small: 14px; 135 | --td-font-size-title-medium: 16px; 136 | --td-font-size-title-large: 20px; 137 | --td-font-size-headline-small: 24px; 138 | --td-font-size-headline-medium: 28px; 139 | --td-font-size-headline-large: 36px; 140 | --td-font-size-display-medium: 48px; 141 | --td-font-size-display-large: 64px; 142 | --td-line-height-common: 8px; 143 | --td-line-height-link-small: calc( var(--td-font-size-link-small) + var(--td-line-height-common) ); 144 | --td-line-height-link-medium: calc( var(--td-font-size-link-medium) + var(--td-line-height-common) ); 145 | --td-line-height-link-large: calc( var(--td-font-size-link-large) + var(--td-line-height-common) ); 146 | --td-line-height-mark-small: calc( var(--td-font-size-mark-small) + var(--td-line-height-common) ); 147 | --td-line-height-mark-medium: calc( var(--td-font-size-mark-medium) + var(--td-line-height-common) ); 148 | --td-line-height-body-small: calc( var(--td-font-size-body-small) + var(--td-line-height-common) ); 149 | --td-line-height-body-medium: calc( var(--td-font-size-body-medium) + var(--td-line-height-common) ); 150 | --td-line-height-body-large: calc( var(--td-font-size-body-large) + var(--td-line-height-common) ); 151 | --td-line-height-title-small: calc( var(--td-font-size-title-small) + var(--td-line-height-common) ); 152 | --td-line-height-title-medium: calc( var(--td-font-size-title-medium) + var(--td-line-height-common) ); 153 | --td-line-height-title-large: calc( var(--td-font-size-title-medium) + var(--td-line-height-common) ); 154 | --td-line-height-headline-small: calc( var(--td-font-size-headline-small) + var(--td-line-height-common) ); 155 | --td-line-height-headline-medium: calc( var(--td-font-size-headline-medium) + var(--td-line-height-common) ); 156 | --td-line-height-headline-large: calc( var(--td-font-size-headline-large) + var(--td-line-height-common) ); 157 | --td-line-height-display-medium: calc( var(--td-font-size-display-medium) + var(--td-line-height-common) ); 158 | --td-line-height-display-large: calc( var(--td-font-size-display-large) + var(--td-line-height-common) ); 159 | --td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small) var(--td-font-family); 160 | --td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium) var(--td-font-family); 161 | --td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large) var(--td-font-family); 162 | --td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small) var(--td-font-family); 163 | --td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium) var(--td-font-family); 164 | --td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small) var(--td-font-family); 165 | --td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium) var(--td-font-family); 166 | --td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large) var(--td-font-family); 167 | --td-font-title-small: var(--td-font-size-title-small) / var(--td-line-height-title-small) var(--td-font-family); 168 | --td-font-title-medium: var(--td-font-size-title-medium) / var(--td-line-height-title-medium) var(--td-font-family); 169 | --td-font-title-large: var(--td-font-size-title-large) / var(--td-line-height-title-large) var(--td-font-family); 170 | --td-font-headline-small: var(--td-font-size-headline-small) / var(--td-line-height-headline-small) var(--td-font-family); 171 | --td-font-headline-medium: var(--td-font-size-headline-medium) / var(--td-line-height-headline-medium) var(--td-font-family); 172 | --td-font-headline-large: var(--td-font-size-headline-large) / var(--td-line-height-headline-large) var(--td-font-family); 173 | --td-font-display-medium: var(--td-font-size-display-medium) / var(--td-line-height-display-medium) var(--td-font-family); 174 | --td-font-display-large: var(--td-font-size-display-large) / var(--td-line-height-display-large) var(--td-font-family); 175 | /* 字体颜色 */ 176 | --td-text-color-primary: var(--td-font-gray-1); 177 | --td-text-color-secondary: var(--td-font-gray-2); 178 | --td-text-color-placeholder: var(--td-font-gray-3); 179 | --td-text-color-disabled: var(--td-font-gray-4); 180 | --td-text-color-anti: #fff; 181 | --td-text-color-brand: var(--td-brand-color); 182 | --td-text-color-link: var(--td-brand-color); 183 | /* end 字体配置 */ /* 圆角配置 */ 184 | --td-radius-small: 2px; 185 | --td-radius-default: 3px; 186 | --td-radius-medium: 6px; 187 | --td-radius-large: 9px; 188 | --td-radius-extraLarge: 12px; 189 | --td-radius-round: 999px; 190 | --td-radius-circle: 50%; 191 | /* end 圆角配置 *//* 阴影配置 */ 192 | --td-shadow-1: 0 1px 10px rgba(0, 0, 0, 0.05), 0 4px 5px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.12); 193 | --td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 0.05), 0 8px 10px 1px rgba(0, 0, 0, 0.06), 0 5px 5px -3px rgba(0, 0, 0, 0.1); 194 | --td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 0.05), 0 16px 24px 2px rgba(0, 0, 0, 0.04), 0 8px 10px -5px rgba(0, 0, 0, 0.08); 195 | /* end 阴影配置 *//* 尺寸配置 */ 196 | --td-size-1: 2px; 197 | --td-size-2: 4px; 198 | --td-size-3: 6px; 199 | --td-size-4: 8px; 200 | --td-size-5: 12px; 201 | --td-size-6: 16px; 202 | --td-size-7: 20px; 203 | --td-size-8: 24px; 204 | --td-size-9: 28px; 205 | --td-size-10: 32px; 206 | --td-size-11: 36px; 207 | --td-size-12: 40px; 208 | --td-size-13: 48px; 209 | --td-size-14: 56px; 210 | --td-size-15: 64px; 211 | --td-size-16: 72px; 212 | --td-comp-size-xxxs: var(--td-size-6); 213 | --td-comp-size-xxs: var(--td-size-7); 214 | --td-comp-size-xs: var(--td-size-8); 215 | --td-comp-size-s: var(--td-size-9); 216 | --td-comp-size-m: var(--td-size-10); 217 | --td-comp-size-l: var(--td-size-11); 218 | --td-comp-size-xl: var(--td-size-12); 219 | --td-comp-size-xxl: var(--td-size-13); 220 | --td-comp-size-xxxl: var(--td-size-14); 221 | --td-comp-size-xxxxl: var(--td-size-15); 222 | --td-comp-size-xxxxxl: var(--td-size-16); 223 | --td-pop-padding-s: var(--td-size-2); 224 | --td-pop-padding-m: var(--td-size-3); 225 | --td-pop-padding-l: var(--td-size-4); 226 | --td-pop-padding-xl: var(--td-size-5); 227 | --td-pop-padding-xxl: var(--td-size-6); 228 | --td-comp-paddingLR-xxs: var(--td-size-1); 229 | --td-comp-paddingLR-xs: var(--td-size-2); 230 | --td-comp-paddingLR-s: var(--td-size-4); 231 | --td-comp-paddingLR-m: var(--td-size-5); 232 | --td-comp-paddingLR-l: var(--td-size-6); 233 | --td-comp-paddingLR-xl: var(--td-size-8); 234 | --td-comp-paddingLR-xxl: var(--td-size-10); 235 | --td-comp-paddingTB-xxs: var(--td-size-1); 236 | --td-comp-paddingTB-xs: var(--td-size-2); 237 | --td-comp-paddingTB-s: var(--td-size-4); 238 | --td-comp-paddingTB-m: var(--td-size-5); 239 | --td-comp-paddingTB-l: var(--td-size-6); 240 | --td-comp-paddingTB-xl: var(--td-size-8); 241 | --td-comp-paddingTB-xxl: var(--td-size-10); 242 | --td-comp-margin-xxs: var(--td-size-1); 243 | --td-comp-margin-xs: var(--td-size-2); 244 | --td-comp-margin-s: var(--td-size-4); 245 | --td-comp-margin-m: var(--td-size-5); 246 | --td-comp-margin-l: var(--td-size-6); 247 | --td-comp-margin-xl: var(--td-size-7); 248 | --td-comp-margin-xxl: var(--td-size-8); 249 | --td-comp-margin-xxxl: var(--td-size-10); 250 | --td-comp-margin-xxxxl: var(--td-size-12); 251 | /* end 尺寸配置 */ 252 | } 253 | 254 | :root[theme-mode="dark"] { 255 | --brand-main: var(--td-brand-color-6); 256 | --td-brand-color-light: var(--td-brand-color-1); 257 | --td-brand-color-focus: var(--td-brand-color-2); 258 | --td-brand-color-disabled: var(--td-brand-color-3); 259 | --td-brand-color-hover: var(--td-brand-color-5); 260 | --td-brand-color: var(--td-brand-color-6); 261 | --td-brand-color-active: var(--td-brand-color-7); 262 | --td-brand-color-1: #9e7cfd20; 263 | --td-brand-color-2: #310085; 264 | --td-brand-color-3: #4920a3; 265 | --td-brand-color-4: #633fbe; 266 | --td-brand-color-5: #7957d5; 267 | --td-brand-color-6: #9e7cfd; 268 | --td-brand-color-7: #baa0ff; 269 | --td-brand-color-8: #d4c2ff; 270 | --td-brand-color-9: #eadeff; 271 | --td-brand-color-10: #f9f1ff; 272 | --td-warning-color-1: #4f2a1d; 273 | --td-warning-color-2: #582f21; 274 | --td-warning-color-3: #733c23; 275 | --td-warning-color-4: #a75d2b; 276 | --td-warning-color-5: #cf6e2d; 277 | --td-warning-color-6: #dc7633; 278 | --td-warning-color-7: #e8935c; 279 | --td-warning-color-8: #ecbf91; 280 | --td-warning-color-9: #eed7bf; 281 | --td-warning-color-10: #f3e9dc; 282 | --td-error-color-1: #472324; 283 | --td-error-color-2: #5e2a2d; 284 | --td-error-color-3: #703439; 285 | --td-error-color-4: #83383e; 286 | --td-error-color-5: #a03f46; 287 | --td-error-color-6: #c64751; 288 | --td-error-color-7: #de6670; 289 | --td-error-color-8: #ec888e; 290 | --td-error-color-9: #edb1b6; 291 | --td-error-color-10: #eeced0; 292 | --td-success-color-1: #193a2a; 293 | --td-success-color-2: #1a4230; 294 | --td-success-color-3: #17533d; 295 | --td-success-color-4: #0d7a55; 296 | --td-success-color-5: #059465; 297 | --td-success-color-6: #43af8a; 298 | --td-success-color-7: #46bf96; 299 | --td-success-color-8: #80d2b6; 300 | --td-success-color-9: #b4e1d3; 301 | --td-success-color-10: #deede8; 302 | --td-gray-color-1: #f3f3f3; 303 | --td-gray-color-2: #eee; 304 | --td-gray-color-3: #e7e7e7; 305 | --td-gray-color-4: #dcdcdc; 306 | --td-gray-color-5: #c5c5c5; 307 | --td-gray-color-6: #a6a6a6; 308 | --td-gray-color-7: #8b8b8b; 309 | --td-gray-color-8: #777; 310 | --td-gray-color-9: #5e5e5e; 311 | --td-gray-color-10: #4b4b4b; 312 | --td-gray-color-11: #383838; 313 | --td-gray-color-12: #2c2c2c; 314 | --td-gray-color-13: #242424; 315 | --td-gray-color-14: #181818; 316 | --td-bg-color-page: var(--td-gray-color-14); 317 | --td-bg-color-container: var(--td-gray-color-13); 318 | --td-bg-color-container-hover: var(--td-gray-color-12); 319 | --td-bg-color-container-active: var(--td-gray-color-10); 320 | --td-bg-color-container-select: var(--td-gray-color-9); 321 | --td-bg-color-secondarycontainer: var(--td-gray-color-12); 322 | --td-bg-color-secondarycontainer-hover: var(--td-gray-color-11); 323 | --td-bg-color-secondarycontainer-active: var(--td-gray-color-9); 324 | --td-bg-color-component: var(--td-gray-color-11); 325 | --td-bg-color-component-hover: var(--td-gray-color-10); 326 | --td-bg-color-component-active: var(--td-gray-color-9); 327 | --td-bg-color-component-disabled: var(--td-gray-color-12); 328 | --td-component-stroke: var(--td-gray-color-11); 329 | --td-component-border: var(--td-gray-color-9); 330 | --td-font-white-1: rgba(255, 255, 255, 0.9); 331 | --td-font-white-2: rgba(255, 255, 255, 0.55); 332 | --td-font-white-3: rgba(255, 255, 255, 0.35); 333 | --td-font-white-4: rgba(255, 255, 255, 0.22); 334 | --td-font-gray-1: rgba(0, 0, 0, 0.9); 335 | --td-font-gray-2: rgba(0, 0, 0, 0.6); 336 | --td-font-gray-3: rgba(0, 0, 0, 0.4); 337 | --td-font-gray-4: rgba(0, 0, 0, 0.26); 338 | --td-gray-color-1: #f3f3f3; 339 | --td-gray-color-2: #eee; 340 | --td-gray-color-3: #e7e7e7; 341 | --td-gray-color-4: #dcdcdc; 342 | --td-gray-color-5: #c5c5c5; 343 | --td-gray-color-6: #a6a6a6; 344 | --td-gray-color-7: #8b8b8b; 345 | --td-gray-color-8: #777; 346 | --td-gray-color-9: #5e5e5e; 347 | --td-gray-color-10: #4b4b4b; 348 | --td-gray-color-11: #383838; 349 | --td-gray-color-12: #2c2c2c; 350 | --td-gray-color-13: #242424; 351 | --td-gray-color-14: #181818; 352 | --td-bg-color-page: var(--td-gray-color-14); 353 | --td-bg-color-container: var(--td-gray-color-13); 354 | --td-bg-color-container-hover: var(--td-gray-color-12); 355 | --td-bg-color-container-active: var(--td-gray-color-10); 356 | --td-bg-color-container-select: var(--td-gray-color-9); 357 | --td-bg-color-secondarycontainer: var(--td-gray-color-12); 358 | --td-bg-color-secondarycontainer-hover: var(--td-gray-color-11); 359 | --td-bg-color-secondarycontainer-active: var(--td-gray-color-9); 360 | --td-bg-color-component: var(--td-gray-color-11); 361 | --td-bg-color-component-hover: var(--td-gray-color-10); 362 | --td-bg-color-component-active: var(--td-gray-color-9); 363 | --td-bg-color-secondarycomponent: var(--td-gray-color-10); 364 | --td-bg-color-secondarycomponent-hover: var(--td-gray-color-9); 365 | --td-bg-color-secondarycomponent-active: var(--td-gray-color-8); 366 | --td-bg-color-component-disabled: var(--td-gray-color-12); 367 | --td-component-stroke: var(--td-gray-color-11); 368 | --td-component-border: var(--td-gray-color-9); 369 | --td-font-white-1: rgba(255, 255, 255, 0.9); 370 | --td-font-white-2: rgba(255, 255, 255, 0.55); 371 | --td-font-white-3: rgba(255, 255, 255, 0.35); 372 | --td-font-white-4: rgba(255, 255, 255, 0.22); 373 | --td-font-gray-1: rgba(0, 0, 0, 0.9); 374 | --td-font-gray-2: rgba(0, 0, 0, 0.6); 375 | --td-font-gray-3: rgba(0, 0, 0, 0.4); 376 | --td-font-gray-4: rgba(0, 0, 0, 0.26); 377 | --td-text-color-primary: var(--td-font-white-1); 378 | --td-text-color-secondary: var(--td-font-white-2); 379 | --td-text-color-placeholder: var(--td-font-white-3); 380 | --td-text-color-disabled: var(--td-font-white-4); 381 | --td-text-color-anti: #fff; 382 | --td-text-color-brand: var(--td-brand-color); 383 | --td-text-color-link: var(--td-brand-color); 384 | --td-table-shadow-color: rgba(0, 0, 0, 55%); 385 | --td-scrollbar-color: rgba(255, 255, 255, 10%); 386 | --td-scrollbar-hover-color: rgba(255, 255, 255, 30%); 387 | --td-scroll-track-color: #333; 388 | --td-bg-color-specialcomponent: transparent; 389 | --td-border-level-1-color: var(--td-gray-color-11); 390 | --td-border-level-2-color: var(--td-gray-color-9); 391 | --td-mask-active: rgba(0, 0, 0, 0.4); 392 | --td-mask-disabled: rgba(0, 0, 0, 0.6); 393 | --td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e; 394 | --td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e; 395 | --td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e; 396 | --td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e; 397 | /* 圆角配置 */ 398 | --td-radius-small: 2px; 399 | --td-radius-default: 3px; 400 | --td-radius-medium: 6px; 401 | --td-radius-large: 9px; 402 | --td-radius-extraLarge: 12px; 403 | --td-radius-round: 999px; 404 | --td-radius-circle: 50%; 405 | /* end 圆角配置 *//* 阴影配置 */ 406 | --td-shadow-1: 0 1px 10px rgba(0, 0, 0, 0.05), 0 4px 5px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.12); 407 | --td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 0.05), 0 8px 10px 1px rgba(0, 0, 0, 0.06), 0 5px 5px -3px rgba(0, 0, 0, 0.1); 408 | --td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 0.05), 0 16px 24px 2px rgba(0, 0, 0, 0.04), 0 8px 10px -5px rgba(0, 0, 0, 0.08); 409 | /* end 阴影配置 *//* 尺寸配置 */ 410 | --td-size-1: 2px; 411 | --td-size-2: 4px; 412 | --td-size-3: 6px; 413 | --td-size-4: 8px; 414 | --td-size-5: 12px; 415 | --td-size-6: 16px; 416 | --td-size-7: 20px; 417 | --td-size-8: 24px; 418 | --td-size-9: 28px; 419 | --td-size-10: 32px; 420 | --td-size-11: 36px; 421 | --td-size-12: 40px; 422 | --td-size-13: 48px; 423 | --td-size-14: 56px; 424 | --td-size-15: 64px; 425 | --td-size-16: 72px; 426 | --td-comp-size-xxxs: var(--td-size-6); 427 | --td-comp-size-xxs: var(--td-size-7); 428 | --td-comp-size-xs: var(--td-size-8); 429 | --td-comp-size-s: var(--td-size-9); 430 | --td-comp-size-m: var(--td-size-10); 431 | --td-comp-size-l: var(--td-size-11); 432 | --td-comp-size-xl: var(--td-size-12); 433 | --td-comp-size-xxl: var(--td-size-13); 434 | --td-comp-size-xxxl: var(--td-size-14); 435 | --td-comp-size-xxxxl: var(--td-size-15); 436 | --td-comp-size-xxxxxl: var(--td-size-16); 437 | --td-pop-padding-s: var(--td-size-2); 438 | --td-pop-padding-m: var(--td-size-3); 439 | --td-pop-padding-l: var(--td-size-4); 440 | --td-pop-padding-xl: var(--td-size-5); 441 | --td-pop-padding-xxl: var(--td-size-6); 442 | --td-comp-paddingLR-xxs: var(--td-size-1); 443 | --td-comp-paddingLR-xs: var(--td-size-2); 444 | --td-comp-paddingLR-s: var(--td-size-4); 445 | --td-comp-paddingLR-m: var(--td-size-5); 446 | --td-comp-paddingLR-l: var(--td-size-6); 447 | --td-comp-paddingLR-xl: var(--td-size-8); 448 | --td-comp-paddingLR-xxl: var(--td-size-10); 449 | --td-comp-paddingTB-xxs: var(--td-size-1); 450 | --td-comp-paddingTB-xs: var(--td-size-2); 451 | --td-comp-paddingTB-s: var(--td-size-4); 452 | --td-comp-paddingTB-m: var(--td-size-5); 453 | --td-comp-paddingTB-l: var(--td-size-6); 454 | --td-comp-paddingTB-xl: var(--td-size-8); 455 | --td-comp-paddingTB-xxl: var(--td-size-10); 456 | --td-comp-margin-xxs: var(--td-size-1); 457 | --td-comp-margin-xs: var(--td-size-2); 458 | --td-comp-margin-s: var(--td-size-4); 459 | --td-comp-margin-m: var(--td-size-5); 460 | --td-comp-margin-l: var(--td-size-6); 461 | --td-comp-margin-xl: var(--td-size-7); 462 | --td-comp-margin-xxl: var(--td-size-8); 463 | --td-comp-margin-xxxl: var(--td-size-10); 464 | --td-comp-margin-xxxxl: var(--td-size-12); 465 | /* end 尺寸配置 */ 466 | } -------------------------------------------------------------------------------- /web/src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /web/src/views/Layout.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /web/src/views/Sandbox.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 103 | 104 | 129 | -------------------------------------------------------------------------------- /web/src/views/SandboxModify.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 115 | 116 | 119 | -------------------------------------------------------------------------------- /web/src/views/SignIn.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 82 | 83 | 174 | -------------------------------------------------------------------------------- /web/src/views/Template.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 107 | 108 | 133 | -------------------------------------------------------------------------------- /web/src/views/TemplateModify.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 141 | 142 | 145 | -------------------------------------------------------------------------------- /web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "jsx": "preserve", 7 | "jsxImportSource": "vue", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "allowSyntheticDefaultImports": true, 13 | "lib": [ 14 | "esnext", 15 | "dom" 16 | ], 17 | "types": [ 18 | "vite/client" 19 | ], 20 | "noEmit": true, 21 | "baseUrl": "./", 22 | "paths": { 23 | "@/*": [ 24 | "src/*" 25 | ] 26 | }, 27 | "noImplicitAny": true, 28 | "strictFunctionTypes": true, 29 | "strictBindCallApply": true, 30 | "noImplicitThis": true, 31 | "alwaysStrict": true 32 | }, 33 | "include": [ 34 | "src/**/*.ts", 35 | "src/**/*.d.ts", 36 | "src/**/*.tsx", 37 | "src/**/*.vue", 38 | "src/types/**/*.d.ts", 39 | "node_modules/tdesign-vue-next/global.d.ts" 40 | ], 41 | "references": [ 42 | { 43 | "path": "./tsconfig.node.json" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": [ 9 | "vite.config.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import svgLoader from 'vite-svg-loader'; 4 | import path from 'node:path'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [vue(), svgLoader()], 9 | 10 | css: { 11 | preprocessorOptions: { 12 | less: { 13 | modifyVars: { 14 | hack: `true; @import (reference) "${path.resolve('src/style/variables.less')}";`, 15 | }, 16 | math: 'strict', 17 | javascriptEnabled: true, 18 | }, 19 | }, 20 | }, 21 | 22 | resolve: { 23 | alias: { 24 | '@': path.resolve(__dirname, './src'), 25 | }, 26 | }, 27 | 28 | server: { 29 | port: 3030, 30 | host: '0.0.0.0', 31 | proxy: { 32 | '/api': { 33 | target: 'http://localhost:8080', 34 | changeOrigin: true, 35 | } 36 | } 37 | } 38 | }) 39 | --------------------------------------------------------------------------------