Skip to content

Commit f4b7483

Browse files
authored
Add API to register server (#472)
1 parent 3d6edd6 commit f4b7483

File tree

5 files changed

+190
-0
lines changed

5 files changed

+190
-0
lines changed

.dockerignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.git
2+
.gitignore
3+
docker-compose.yml
4+
Dockerfile
5+
Dockerfile.dev
6+
7+
8+
data/*

Dockerfile.dev

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Use build arguments for Go version and architecture
2+
ARG GO_VERSION=1.22
3+
ARG BUILDARCH=amd64
4+
5+
# Stage 1: Builder Stage
6+
# FROM golang:${GO_VERSION}-alpine AS builder
7+
FROM crazymax/xgo:${GO_VERSION} AS builder
8+
9+
# Set up working directory
10+
WORKDIR /app
11+
12+
# Step 1: Copy the source code
13+
COPY . .
14+
15+
# use --mount=type=cache,target=/go/pkg/mod to cache the go mod
16+
# Step 2: Download dependencies
17+
RUN --mount=type=cache,target=/go/pkg/mod \
18+
go mod tidy && go mod download
19+
20+
# Step 3: Build the Go application with CGO enabled and specified ldflags
21+
RUN --mount=type=cache,target=/go/pkg/mod \
22+
CGO_ENABLED=1 GOOS=linux go build -a \
23+
-ldflags "-s -w --extldflags '-static -fpic'" \
24+
-installsuffix cgo -o dashboard cmd/dashboard/main.go
25+
26+
27+
# Stage 2: Create the final image
28+
FROM alpine:latest
29+
30+
ARG COUNTRY
31+
# Install required tools without caching index to minimize image size
32+
RUN if [ "$COUNTRY" = "CN" ] ; then \
33+
echo "It is in China, updating the repositories"; \
34+
sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirrors.tuna.tsinghua.edu.cn/alpine#g' /etc/apk/repositories; \
35+
fi && \
36+
apk update && apk add --no-cache tzdata && \
37+
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
38+
echo 'Asia/Shanghai' >/etc/timezone && \
39+
rm -rf /var/cache/apk/* && \
40+
mkdir -p /dashboard/data
41+
42+
43+
# Copy the entrypoint script and ensure it is executable
44+
COPY ./script/entrypoint.sh /entrypoint.sh
45+
46+
# Set up the entrypoint script
47+
RUN chmod +x /entrypoint.sh
48+
49+
WORKDIR /dashboard
50+
51+
# Copy the statically linked binary from the builder stage
52+
COPY --from=builder /app/dashboard ./app
53+
# Copy the configuration file and the resource directory
54+
COPY ./script/config.yaml ./data/config.yaml
55+
COPY ./resource ./resource
56+
57+
58+
# Set up volume and expose ports
59+
VOLUME ["/dashboard/data"]
60+
EXPOSE 80 5555 443
61+
62+
# Define the entrypoint
63+
ENTRYPOINT ["/entrypoint.sh"]

cmd/dashboard/controller/api_v1.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func (v *apiV1) serve() {
2828
}))
2929
r.GET("/server/list", v.serverList)
3030
r.GET("/server/details", v.serverDetails)
31+
r.POST("/server/register", v.RegisterServer)
3132
// 不强制认证的 API
3233
mr := v.r.Group("monitor")
3334
mr.Use(mygin.Authorize(mygin.AuthorizeOption{
@@ -83,6 +84,45 @@ func (v *apiV1) serverDetails(c *gin.Context) {
8384
c.JSON(200, singleton.ServerAPI.GetAllStatus())
8485
}
8586

87+
// RegisterServer adds a server and responds with the full ServerRegisterResponse
88+
// header: Authorization: Token
89+
// body: RegisterServer
90+
// response: ServerRegisterResponse or Secret string
91+
func (v *apiV1) RegisterServer(c *gin.Context) {
92+
var rs singleton.RegisterServer
93+
// Attempt to bind JSON to RegisterServer struct
94+
if err := c.ShouldBindJSON(&rs); err != nil {
95+
c.JSON(400, singleton.ServerRegisterResponse{
96+
CommonResponse: singleton.CommonResponse{
97+
Code: 400,
98+
Message: "Parse JSON failed",
99+
},
100+
})
101+
return
102+
}
103+
// Check if simple mode is requested
104+
simple := c.Query("simple") == "true" || c.Query("simple") == "1"
105+
// Set defaults if fields are empty
106+
if rs.Name == "" {
107+
rs.Name = c.ClientIP()
108+
}
109+
if rs.Tag == "" {
110+
rs.Tag = "AutoRegister"
111+
}
112+
if rs.HideForGuest == "" {
113+
rs.HideForGuest = "on"
114+
}
115+
// Call the Register function and get the response
116+
response := singleton.ServerAPI.Register(&rs)
117+
// Respond with Secret only if in simple mode, otherwise full response
118+
if simple {
119+
c.JSON(response.Code, response.Secret)
120+
} else {
121+
c.JSON(response.Code, response)
122+
}
123+
}
124+
125+
86126
func (v *apiV1) monitorHistoriesById(c *gin.Context) {
87127
idStr := c.Param("id")
88128
id, err := strconv.ParseUint(idStr, 10, 64)

docker-compose.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
services:
2+
app:
3+
build:
4+
context: .
5+
dockerfile: Dockerfile.dev
6+
args:
7+
COUNTRY: CN
8+
image: nezha:dev
9+
container_name: nezha-dev
10+
ports:
11+
- ${NEZHA_PORT:-80}:18080
12+
- 5555:5555
13+
volumes:
14+
- /etc/timezone:/etc/timezone:ro
15+
- /etc/localtime:/etc/localtime:ro
16+
- ./data:/dashboard/data
17+
# - ./resource:/dashboard/resource

service/singleton/api.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ type CommonResponse struct {
2525
Message string `json:"message"`
2626
}
2727

28+
type RegisterServer struct {
29+
Name string
30+
Tag string
31+
Note string
32+
HideForGuest string
33+
}
34+
35+
type ServerRegisterResponse struct {
36+
CommonResponse
37+
Secret string `json:"secret"`
38+
}
39+
40+
2841
type CommonServerInfo struct {
2942
ID uint64 `json:"id"`
3043
Name string `json:"name"`
@@ -227,6 +240,55 @@ func (s *ServerAPIService) GetAllList() *ServerInfoResponse {
227240
}
228241
return res
229242
}
243+
func (s *ServerAPIService) Register(rs *RegisterServer) *ServerRegisterResponse {
244+
var serverInfo model.Server
245+
var err error
246+
// Populate serverInfo fields
247+
serverInfo.Name = rs.Name
248+
serverInfo.Tag = rs.Tag
249+
serverInfo.Note = rs.Note
250+
serverInfo.HideForGuest = rs.HideForGuest == "on"
251+
// Generate a random secret
252+
serverInfo.Secret, err = utils.GenerateRandomString(18)
253+
if err != nil {
254+
return &ServerRegisterResponse{
255+
CommonResponse: CommonResponse{
256+
Code: 500,
257+
Message: "Generate secret failed: " + err.Error(),
258+
},
259+
Secret: "",
260+
}
261+
}
262+
// Attempt to save serverInfo in the database
263+
err = DB.Create(&serverInfo).Error
264+
if err != nil {
265+
return &ServerRegisterResponse{
266+
CommonResponse: CommonResponse{
267+
Code: 500,
268+
Message: "Database error: " + err.Error(),
269+
},
270+
Secret: "",
271+
}
272+
}
273+
274+
serverInfo.Host = &model.Host{}
275+
serverInfo.State = &model.HostState{}
276+
serverInfo.TaskCloseLock = new(sync.Mutex)
277+
ServerLock.Lock()
278+
SecretToID[serverInfo.Secret] = serverInfo.ID
279+
ServerList[serverInfo.ID] = &serverInfo
280+
ServerTagToIDList[serverInfo.Tag] = append(ServerTagToIDList[serverInfo.Tag], serverInfo.ID)
281+
ServerLock.Unlock()
282+
ReSortServer()
283+
// Successful response
284+
return &ServerRegisterResponse{
285+
CommonResponse: CommonResponse{
286+
Code: 200,
287+
Message: "Server created successfully",
288+
},
289+
Secret: serverInfo.Secret,
290+
}
291+
}
230292

231293
func (m *MonitorAPIService) GetMonitorHistories(query map[string]any) *MonitorInfoResponse {
232294
var (

0 commit comments

Comments
 (0)