diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2bda27d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.gitignore +docker-compose.yml +Dockerfile +Dockerfile.dev + + +data/* \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..53ed073 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,63 @@ +# Use build arguments for Go version and architecture +ARG GO_VERSION=1.22 +ARG BUILDARCH=amd64 + +# Stage 1: Builder Stage +# FROM golang:${GO_VERSION}-alpine AS builder +FROM crazymax/xgo:${GO_VERSION} AS builder + +# Set up working directory +WORKDIR /app + +# Step 1: Copy the source code +COPY . . + +# use --mount=type=cache,target=/go/pkg/mod to cache the go mod +# Step 2: Download dependencies +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod tidy && go mod download + +# Step 3: Build the Go application with CGO enabled and specified ldflags +RUN --mount=type=cache,target=/go/pkg/mod \ + CGO_ENABLED=1 GOOS=linux go build -a \ + -ldflags "-s -w --extldflags '-static -fpic'" \ + -installsuffix cgo -o dashboard cmd/dashboard/main.go + + +# Stage 2: Create the final image +FROM alpine:latest + +ARG COUNTRY +# Install required tools without caching index to minimize image size +RUN if [ "$COUNTRY" = "CN" ] ; then \ + echo "It is in China, updating the repositories"; \ + sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirrors.tuna.tsinghua.edu.cn/alpine#g' /etc/apk/repositories; \ + fi && \ + apk update && apk add --no-cache tzdata && \ + cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ + echo 'Asia/Shanghai' >/etc/timezone && \ + rm -rf /var/cache/apk/* && \ + mkdir -p /dashboard/data + + +# Copy the entrypoint script and ensure it is executable +COPY ./script/entrypoint.sh /entrypoint.sh + +# Set up the entrypoint script +RUN chmod +x /entrypoint.sh + +WORKDIR /dashboard + +# Copy the statically linked binary from the builder stage +COPY --from=builder /app/dashboard ./app +# Copy the configuration file and the resource directory +COPY ./script/config.yaml ./data/config.yaml +COPY ./resource ./resource + + +# Set up volume and expose ports +VOLUME ["/dashboard/data"] +EXPOSE 80 5555 443 + +# Define the entrypoint +ENTRYPOINT ["/entrypoint.sh"] diff --git a/cmd/dashboard/controller/api_v1.go b/cmd/dashboard/controller/api_v1.go index 83e0bd3..0bef3ab 100644 --- a/cmd/dashboard/controller/api_v1.go +++ b/cmd/dashboard/controller/api_v1.go @@ -28,6 +28,7 @@ func (v *apiV1) serve() { })) r.GET("/server/list", v.serverList) r.GET("/server/details", v.serverDetails) + r.POST("/server/register", v.RegisterServer) // 不强制认证的 API mr := v.r.Group("monitor") mr.Use(mygin.Authorize(mygin.AuthorizeOption{ @@ -83,6 +84,45 @@ func (v *apiV1) serverDetails(c *gin.Context) { c.JSON(200, singleton.ServerAPI.GetAllStatus()) } +// RegisterServer adds a server and responds with the full ServerRegisterResponse +// header: Authorization: Token +// body: RegisterServer +// response: ServerRegisterResponse or Secret string +func (v *apiV1) RegisterServer(c *gin.Context) { + var rs singleton.RegisterServer + // Attempt to bind JSON to RegisterServer struct + if err := c.ShouldBindJSON(&rs); err != nil { + c.JSON(400, singleton.ServerRegisterResponse{ + CommonResponse: singleton.CommonResponse{ + Code: 400, + Message: "Parse JSON failed", + }, + }) + return + } + // Check if simple mode is requested + simple := c.Query("simple") == "true" || c.Query("simple") == "1" + // Set defaults if fields are empty + if rs.Name == "" { + rs.Name = c.ClientIP() + } + if rs.Tag == "" { + rs.Tag = "AutoRegister" + } + if rs.HideForGuest == "" { + rs.HideForGuest = "on" + } + // Call the Register function and get the response + response := singleton.ServerAPI.Register(&rs) + // Respond with Secret only if in simple mode, otherwise full response + if simple { + c.JSON(response.Code, response.Secret) + } else { + c.JSON(response.Code, response) + } +} + + func (v *apiV1) monitorHistoriesById(c *gin.Context) { idStr := c.Param("id") id, err := strconv.ParseUint(idStr, 10, 64) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..560cc78 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile.dev + args: + COUNTRY: CN + image: nezha:dev + container_name: nezha-dev + ports: + - ${NEZHA_PORT:-80}:18080 + - 5555:5555 + volumes: + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + - ./data:/dashboard/data + # - ./resource:/dashboard/resource \ No newline at end of file diff --git a/service/singleton/api.go b/service/singleton/api.go index f86cb72..5adf4c1 100644 --- a/service/singleton/api.go +++ b/service/singleton/api.go @@ -25,6 +25,19 @@ type CommonResponse struct { Message string `json:"message"` } +type RegisterServer struct { + Name string + Tag string + Note string + HideForGuest string +} + +type ServerRegisterResponse struct { + CommonResponse + Secret string `json:"secret"` +} + + type CommonServerInfo struct { ID uint64 `json:"id"` Name string `json:"name"` @@ -227,6 +240,55 @@ func (s *ServerAPIService) GetAllList() *ServerInfoResponse { } return res } +func (s *ServerAPIService) Register(rs *RegisterServer) *ServerRegisterResponse { + var serverInfo model.Server + var err error + // Populate serverInfo fields + serverInfo.Name = rs.Name + serverInfo.Tag = rs.Tag + serverInfo.Note = rs.Note + serverInfo.HideForGuest = rs.HideForGuest == "on" + // Generate a random secret + serverInfo.Secret, err = utils.GenerateRandomString(18) + if err != nil { + return &ServerRegisterResponse{ + CommonResponse: CommonResponse{ + Code: 500, + Message: "Generate secret failed: " + err.Error(), + }, + Secret: "", + } + } + // Attempt to save serverInfo in the database + err = DB.Create(&serverInfo).Error + if err != nil { + return &ServerRegisterResponse{ + CommonResponse: CommonResponse{ + Code: 500, + Message: "Database error: " + err.Error(), + }, + Secret: "", + } + } + + serverInfo.Host = &model.Host{} + serverInfo.State = &model.HostState{} + serverInfo.TaskCloseLock = new(sync.Mutex) + ServerLock.Lock() + SecretToID[serverInfo.Secret] = serverInfo.ID + ServerList[serverInfo.ID] = &serverInfo + ServerTagToIDList[serverInfo.Tag] = append(ServerTagToIDList[serverInfo.Tag], serverInfo.ID) + ServerLock.Unlock() + ReSortServer() + // Successful response + return &ServerRegisterResponse{ + CommonResponse: CommonResponse{ + Code: 200, + Message: "Server created successfully", + }, + Secret: serverInfo.Secret, + } +} func (m *MonitorAPIService) GetMonitorHistories(query map[string]any) *MonitorInfoResponse { var (