diff --git a/README.md b/README.md index c0fa8df..cc53375 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,15 @@ My App is a continuously updated personal service collection. Prepare and install environment for development in Window 10/11? +- Git v2.37+, https://git-scm.com/ - Go v1.19+, https://go.dev/ - Node.js v16+, https://nodejs.org/ - PNPM v7+, https://pnpm.io/ - VS Code v1.71+ with GCC, https://code.visualstudio.com/docs/cpp/config-mingw - WebView2 v104+, https://developer.microsoft.com/en-us/microsoft-edge/webview2/ +> Setup VS Code by installing recommended extensions. To do this, enter in `@recommended` while searching for extensions. + > Run command `go env -w CGO_ENABLED=1` to prepare for _CGO_ enabled packages > Run command `pnpm install` at project root directory to setup. diff --git a/backend/model/model.go b/backend/model/model.go index ea5a648..d833fde 100644 --- a/backend/model/model.go +++ b/backend/model/model.go @@ -25,5 +25,7 @@ func init() { log.Fatalf("failed to connect database: %+v\n", err) } - db.AutoMigrate() + db.AutoMigrate( + &MyOption{}, + ) } diff --git a/backend/model/my_option.go b/backend/model/my_option.go new file mode 100644 index 0000000..95e420a --- /dev/null +++ b/backend/model/my_option.go @@ -0,0 +1,25 @@ +package model + +import "gorm.io/gorm" + +type MyOption struct { + gorm.Model + Name string `gorm:"unique"` // Option name + Value string `` // Option value associated with name +} + +func (mo *MyOption) Update(newValue string) *gorm.DB { + return db.Model(mo).Where(mo).Updates(MyOption{ + Value: newValue, + }) +} + +type MyOptions []MyOption + +func (mos MyOptions) Load() *gorm.DB { + return db.Find(&mos) +} + +func (mos MyOptions) Save() *gorm.DB { + return db.Save(&mos) +} diff --git a/backend/web/air.go b/backend/web/air.go index 4a8b8a5..570adfb 100644 --- a/backend/web/air.go +++ b/backend/web/air.go @@ -1,14 +1,25 @@ package web -import "flag" +import ( + // "context" + "flag" + // "time" +) -// for API service test purpose, testing with air +// For API service test purpose, testing with air +// Uncomment code below to run http redirector func (w *web) Air() { var flagAir int flag.IntVar(&flagAir, "air", 0, "set `-air 1` to enable web.Air function") flag.Parse() if flagAir == 1 { w.reset() + // go w.http.ListenAndServe() w.https.ListenAndServeTLS("", "") + + // ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + // defer cancel() + // w.http.Shutdown(ctx) + // <-ctx.Done() } } diff --git a/backend/web/api/test/test.go b/backend/web/api/test/test.go new file mode 100644 index 0000000..57fbb95 --- /dev/null +++ b/backend/web/api/test/test.go @@ -0,0 +1,20 @@ +package test + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// @Summary Pass +// @Description Test pass path +// @Tags Test +// @Accept json +// @Produce json +// @Success 200 {string} string "Pass Path" +// @Router /test [get] +func Test() gin.HandlerFunc { + return func(ctx *gin.Context) { + ctx.String(http.StatusOK, "Pass "+ctx.Request.URL.Path) + } +} diff --git a/backend/web/auth/login.go b/backend/web/auth/login.go new file mode 100644 index 0000000..52189e7 --- /dev/null +++ b/backend/web/auth/login.go @@ -0,0 +1,21 @@ +package auth + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// @Summary Login +// @Description Login and get access token +// @Tags Auth +// @Accept x-www-form-urlencoded +// @Produce json +// @Param username formData string true "Username" +// @Param password formData string true "Password" +// @Router /auth/login [post] +func Login() gin.HandlerFunc { + return func(ctx *gin.Context) { + ctx.JSON(http.StatusOK, gin.H{}) + } +} diff --git a/backend/web/certs/localhost.crt b/backend/web/certs/localhost.crt index 97593e5..dc28d4b 100644 --- a/backend/web/certs/localhost.crt +++ b/backend/web/certs/localhost.crt @@ -1,23 +1,19 @@ -----BEGIN CERTIFICATE----- -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 +3DbsOJaCgMVGpv/SMGkJ -----END CERTIFICATE----- diff --git a/backend/web/certs/localhost.key b/backend/web/certs/localhost.key index d001431..275a6c4 100644 --- a/backend/web/certs/localhost.key +++ b/backend/web/certs/localhost.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCqVmq8voUpyqT6 -crTeghGbcBOHF7Liw3pf10hz26TCQPFgTwdGrZY8j/W5su8Sb1MhfVVo1pVWSpyt -veVIcDK8Dtxs+9zKmnJLp4gzDr29eHAWCnyaoDzX5fhgCJBqBvzrW5g8PbjWKrFf -B9BxSUmVutkDyslfx1K3GUPNDNNYUcPx85ira7BDPd7v/0MA4J8AXJ8eOiWduYdS -rhZS3uq7yx2VOPpU2HUc5xB93oCKY3MJ+8Gw6I+comP6XSkzrs1DYaJEiULVhm/p -pcqaQRNra6hvKIBhur5qjpD8usTcSeGjGBTSD8P22oRmb9FCqQIeM9uJyeM6h/MT -GRxKZkQ9AgMBAAECggEBAJYUhAD+4Hc8+/VKwb/W3EqXCi2aHNwdnaH6HfUuzlD1 -RXc0ylVktp1vvfK7DFeyzI0SUAGKJ5QbM0cFrJIRgTIe6eoAU74TLKcp2iAaWeUp -y6Av5y+aBWG4VwnOIee5dnisgEp7m3LftN3lFavrBbGuKm1j69++0EtRMGLnbQM/ -vwL9GHvgzoDllhTPui6yeZcQhVjlcIwxD5B72qwS/XYWrhNIjICVVsBQjm3U+VrT -QO9ItmZdKnJ61t9ShzrwOxmbzq4RVVJMcl7CI8EhbQQaaPCUQK+Yl4atmf7HNUqN -BnVryJT/yI6tPKRWuNov0Cy9LJQKxV9JpV9TZapTpAUCgYEA3ZudMqrj0pUbwupa -R+cFTKAS+5u1lTimz2wK9ZcLJQ557LGe6w7K9ClD0/UkjUch00XtuN3z/nAjmD5q -Mhnptfzzk2NxY4RxgBjlVv3mFE0zzQelgLO8WjpROkkkV412RgZxzRXM1REawoPb -ezZVfvVQ4kqXOrJL/KffOwOb6pMCgYEAxMXbEALf7hgXjbT5duhFLqB+ATDiyTKZ -pGsBJ+zQVFcbu1q3dW/sPzyEfwMDGMHXcuEb1AsrOAqGaA+XzWydkQ/3LbtsGftK -llfjuef5zltTqdND/DDzqXJHjye9A+v2xhq8RSJJWtKx7rfHBLHphB+uZoe38F3t -BJIM/K5Kx+8CgYEAoviBznUmPgMx+1HlOuOGXieKTkbgbvDOm9PU62883u+acprd -R/gFATInk4TAcLVTBtByVnXDreZCBwA/kMDFwvRXHJkRFiKcZZvpbOQjg+KSqcFp -0RBc/+3LNpX7h/ecdzreDfhuPnLpvwrBKgd3MHqwFPrN5HiisRezbu7khskCgYA2 -X+5fCExVAPdRQ1dEUn77mYH4Vkf1DTSDyMXzAG+5PrD9Ht5fZ2RDPTfn2S874iTl -K+uCtutkexQVIWnzbDZGZcEKNCZ1L4m27eaR8taG/Zmq7iR1RhmvE1NO7c4/jS8O -I0kvWYAnxt1AeCxY0ckTp6WL06kylOHwR2OrV4Z2pQKBgHkm+pWuMJizccRXhuYg -VfR9dCfY6NNz6fkD+QbA9fuasxNftTb/2S4yF4OFUeZvSOQ8h+TtLS8w73JXnMR3 -w5D569Z7gIqpRXVOMyCmWNguMVuRZpA6Mo7hAoPAWNk7GjW/XfSXQzTRVf0WRZ5z -zLmYOG1EKPx6x0Rp8Pr8hYiw +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDhN02/EzY2pPDB +0EsKMFrhzjT4Y9pSDX/qx7wSxZif29cNqsRRdQ6uNnkMwWv7L97erMXsWjx0nzm4 +rhGcxa+bASursRBcI+u0oQ4AkI0uxBvyguhjhnwxyuMnfpT7zgrMtzirU9wxDwjX +LAmNcKFrWrT0sTaeWlyOWRpswCRyG3n9nGj7JEYOCMK7EhnIcBspLE+3b0v5oYAP +p4arUWieUyHOJ+APeVtI4W9L/LDuLkwrtjCd8JEEke0oMpDP10G1o5H1MjPssARN +6yIqoqanaM7fiukeZeoTJi8ZJrCGyuuOwlvm24o8JXqWTlR0TqDXPgAFoLL3wHYx +Jnxby+G3AgMBAAECggEAb03KsaUIBQeNKOwNOfLd53zmtt96dVTQpDKkI07eeBrI +9KsxrORCJh0rw/8Po6tr5PbUNlP+TtCmUTxN3gHhIKT6dTbj0/W4tqNl7SeDbtpi +yX6i5RUA6gbQLqOjc7LHqZbffeTLDL/WaakM64b/b7P7fHbcfHRCC+PDaH2pRW6b +StZdTRcEJHHYE8E0HDedQB3cbyoIJmtmPhsjp1T+y/z8ChugGZq2FI2IC4ixnM5R ++fluDIAH7on3GiKVlRRGvJJncF0CJazqRd/cyoTxWYeEauKgMdaA+Y6kI5svmZ1q +93gD0TKfYiXksBaK84w1EjJNyljvvDAmgyP5FQ3K+QKBgQD7gL3MKA/Ef7FhlbFV +K7T9LG1gCICJCfXppKh2DibWl9FbmGjHJZHHpQaJtkJMQtqGeRpOO2gFxnOzxR+M +vXcqQaLjrVsyOuFxSfHGekxwtSvl7IdzuywYWmNdqfL743MOLta74Brg4AuqMg7K +F09FlIODzpg59tdGrRXh0PbEcwKBgQDlPjvWlTCeVJ5+q5mhPyynOaMB4sdXluu0 +8mEpjhKUvJFIEhIiXW4EVwYujacA9xxL301n+DWQrxbugyywqA+VcVqOX9UnL3J2 +YZxW73/DPah9mFbkRJvEHFZtGZ+p4eUKOPA9HXUCMZdkjPmrZqOWTLta0NZsphR4 +RuyZfapgrQKBgQCu5rEXMSUF+edZfch8+pA7IJJPnpoEszCY8zFUKDNKOPoXQL5c +//uJY3JQgrdIYZWmvsFUdmu42HHOKt/t+DTO5iZpPz2UiO4O4uWIqbRPDS8iDoOE +MD5SEUnY8T9RfLOewWQD8629hRGXoog2ck9mjAmJuDqU1NyRAf2dtLxnkQKBgAWT +X+02L7qKIFjAX7o2SSXAFyDinSqaNx1tj8Ns+zK262mvVtWTJCvi2fmj5F33pK6L +vdw7g0IDoDEo65tYWxitayBvYEXVt5j9gsnyhU8AXuq/G83thURd1BRPPTzqi6GW +BPHXl5L4FdCSRThQBMaREzWAQtbdqz3Jq9OH5O75AoGAAhoECDGhC6QxTN8aCZgN +LN0rdnNKrCPJJl9xE6Xc4rv8Cwslk5tUiQ0CjfMwhZnAFrZB0rA4eBlRaUPvirpq +mUz765m/DZGPi+5Gj/P7Shs2X8YgFJRzO7lnYWOZRsx4PeNkBK11Xq2pcju8rykk +Bsdr+3HKTnM4gywLnuNFzOg= -----END PRIVATE KEY----- diff --git a/backend/web/config.go b/backend/web/config.go new file mode 100644 index 0000000..902266c --- /dev/null +++ b/backend/web/config.go @@ -0,0 +1,21 @@ +package web + +const ( + CfgPortHttp = Package + ".PortHttp" + CfgPortHttps = Package + ".PortHttps" + CfgDirCerts = Package + ".DirCerts" +) + +type Config struct { + PortHttp string + PortHttps string + DirCerts string +} + +func DefaultConfig() Config { + return Config{ + PortHttp: ":10080", + PortHttps: ":10443", + DirCerts: "", + } +} diff --git a/backend/web/docs/docs.go b/backend/web/docs/docs.go index dce7843..6c9117c 100644 --- a/backend/web/docs/docs.go +++ b/backend/web/docs/docs.go @@ -12,7 +12,8 @@ const docTemplate = `{ "title": "{{.Title}}", "contact": { "name": "Github Issues", - "url": "https://github.com/jinyaoMa/my-app/issues" + "url": "https://github.com/jinyaoMa/my-app/issues", + "email": "jinyao.ma@outlook.com" }, "license": { "name": "MIT", @@ -22,9 +23,34 @@ const docTemplate = `{ }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", - "paths": {}, + "paths": { + "/test": { + "get": { + "description": "Test pass path", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Test" + ], + "summary": "Pass", + "responses": { + "200": { + "description": "Pass Path", + "schema": { + "type": "string" + } + } + } + } + } + }, "securityDefinitions": { "BearerToken": { + "description": "Authorization Header should contain value started with \"Bearer \" and followed by a JSON Web Token.", "type": "apiKey", "name": "Authorization", "in": "header" @@ -37,7 +63,7 @@ var SwaggerInfo = &swag.Spec{ Version: "1.0.0", Host: "", BasePath: "/api", - Schemes: []string{}, + Schemes: []string{"https"}, Title: "My App (backend/web/router.go)", Description: "\"My App is a continuously updated personal service collection.\"", InfoInstanceName: "swagger", diff --git a/backend/web/docs/swagger.json b/backend/web/docs/swagger.json index 3eacb19..05f33a7 100644 --- a/backend/web/docs/swagger.json +++ b/backend/web/docs/swagger.json @@ -1,11 +1,15 @@ { + "schemes": [ + "https" + ], "swagger": "2.0", "info": { "description": "\"My App is a continuously updated personal service collection.\"", "title": "My App (backend/web/router.go)", "contact": { "name": "Github Issues", - "url": "https://github.com/jinyaoMa/my-app/issues" + "url": "https://github.com/jinyaoMa/my-app/issues", + "email": "jinyao.ma@outlook.com" }, "license": { "name": "MIT", @@ -14,9 +18,34 @@ "version": "1.0.0" }, "basePath": "/api", - "paths": {}, + "paths": { + "/test": { + "get": { + "description": "Test pass path", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Test" + ], + "summary": "Pass", + "responses": { + "200": { + "description": "Pass Path", + "schema": { + "type": "string" + } + } + } + } + } + }, "securityDefinitions": { "BearerToken": { + "description": "Authorization Header should contain value started with \"Bearer \" and followed by a JSON Web Token.", "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/backend/web/docs/swagger.yaml b/backend/web/docs/swagger.yaml index e8d67ba..80de75b 100644 --- a/backend/web/docs/swagger.yaml +++ b/backend/web/docs/swagger.yaml @@ -1,6 +1,7 @@ basePath: /api info: contact: + email: jinyao.ma@outlook.com name: Github Issues url: https://github.com/jinyaoMa/my-app/issues description: '"My App is a continuously updated personal service collection."' @@ -9,9 +10,28 @@ info: url: https://github.com/jinyaoMa/my-app/blob/main/LICENSE title: My App (backend/web/router.go) version: 1.0.0 -paths: {} +paths: + /test: + get: + consumes: + - application/json + description: Test pass path + produces: + - application/json + responses: + "200": + description: Pass Path + schema: + type: string + summary: Pass + tags: + - Test +schemes: +- https securityDefinitions: BearerToken: + description: Authorization Header should contain value started with "Bearer " + and followed by a JSON Web Token. in: header name: Authorization type: apiKey diff --git a/backend/web/middleware/auth.go b/backend/web/middleware/auth.go new file mode 100644 index 0000000..d3b1c5a --- /dev/null +++ b/backend/web/middleware/auth.go @@ -0,0 +1,9 @@ +package middleware + +import "github.com/gin-gonic/gin" + +func Auth() gin.HandlerFunc { + return func(ctx *gin.Context) { + ctx.Next() + } +} diff --git a/backend/web/router.go b/backend/web/router.go index e1ded31..64c5577 100644 --- a/backend/web/router.go +++ b/backend/web/router.go @@ -1,11 +1,12 @@ package web import ( - _ "my-app/backend/web/docs" - "net/http" + "my-app/backend/web/api/test" + "my-app/backend/web/docs" + "my-app/backend/web/middleware" "github.com/gin-gonic/gin" - swaggerfiles "github.com/swaggo/files" + swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" ) @@ -15,29 +16,39 @@ import ( // @contact.name Github Issues // @contact.url https://github.com/jinyaoMa/my-app/issues +// @contact.email jinyao.ma@outlook.com // @license.name MIT // @license.url https://github.com/jinyaoMa/my-app/blob/main/LICENSE +// @schemes https // @BasePath /api // @securityDefinitions.apikey BearerToken // @in header // @name Authorization +// @description Authorization Header should contain value started with "Bearer " and followed by a JSON Web Token. func router() *gin.Engine { r := gin.Default() - r.GET("/", func(ctx *gin.Context) { - ctx.JSON(http.StatusOK, gin.H{ - "Author": "pong", - }) - }) + r.GET("/", test.Test()) + + a := r.Group("/auth") + { + a.GET("/", middleware.Auth(), func(ctx *gin.Context) {}) + } + + // "/api" + b := r.Group(docs.SwaggerInfo.BasePath) + { + b.GET("/test", test.Test()) + } r.GET( "/swagger/*any", ginSwagger.WrapHandler( - swaggerfiles.Handler, + swaggerFiles.Handler, ginSwagger.PersistAuthorization(true), ), ) diff --git a/backend/web/web.go b/backend/web/web.go index b9f5781..5ec7b11 100644 --- a/backend/web/web.go +++ b/backend/web/web.go @@ -15,6 +15,8 @@ import ( "golang.org/x/sync/errgroup" ) +const Package = "Web" + //go:embed certs var certs embed.FS @@ -28,15 +30,23 @@ type web struct { errGroup errgroup.Group http *http.Server // redirector https *http.Server // server (tls) + config Config } func Web() *web { once.Do(func() { - instance = &web{} + instance = &web{ + config: DefaultConfig(), + } }) return instance } +func (w *web) SetConfig(cfg Config) *web { + w.config = cfg + return w +} + func (w *web) Start() (ok bool) { if w.isRunning { return false @@ -56,15 +66,18 @@ func (w *web) Start() (ok bool) { func (w *web) Stop() (ok bool) { if w.isRunning { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - if err := w.http.Shutdown(ctx); err != nil && err != http.ErrServerClosed { + ctxHttp, cancelHttp := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelHttp() + if err := w.http.Shutdown(ctxHttp); err != nil && err != http.ErrServerClosed { log.Printf("server (http) shutdown error: %+v\n", err) } - if err := w.https.Shutdown(ctx); err != nil && err != http.ErrServerClosed { + + ctxHttps, cancelHttps := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelHttps() + if err := w.https.Shutdown(ctxHttps); err != nil && err != http.ErrServerClosed { log.Printf("server (http/s) shutdown error: %+v\n", err) } + if err := w.errGroup.Wait(); err != nil && err != http.ErrServerClosed { log.Printf("server running error: %+v\n", err) } @@ -76,18 +89,16 @@ func (w *web) Stop() (ok bool) { } func (w *web) reset() { - portHttp := ":10080" - portHttps := ":10443" - manager := &autocert.Manager{ Prompt: autocert.AcceptTOS, + Cache: autocert.DirCache(w.config.DirCerts), } w.http = &http.Server{ - Addr: portHttp, - Handler: manager.HTTPHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - target := "https://" + strings.Replace(r.Host, portHttp, portHttps, 1) + r.RequestURI - http.Redirect(w, r, target, http.StatusMovedPermanently) + Addr: w.config.PortHttp, + Handler: manager.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + target := "https://" + strings.Replace(r.Host, w.config.PortHttp, w.config.PortHttps, 1) + r.RequestURI + http.Redirect(rw, r, target, http.StatusMovedPermanently) })), } @@ -95,25 +106,33 @@ func (w *web) reset() { tlsConfig.GetCertificate = w.getSelfSignedOrLetsEncryptCert(manager) w.https = &http.Server{ - Addr: portHttps, + Addr: w.config.PortHttps, Handler: router(), TLSConfig: tlsConfig, } } func (s *web) getSelfSignedOrLetsEncryptCert(certManager *autocert.Manager) func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + dirCerts := "certs" + + var certificate tls.Certificate + var err error + isCustomDirCerts := false + dirCache, ok := certManager.Cache.(autocert.DirCache) + if ok && string(dirCache) != "" { + dirCerts = string(dirCache) + isCustomDirCerts = true + } else { + key, _ := certs.ReadFile(dirCerts + "/localhost.key") // embed use slash as separator + crt, _ := certs.ReadFile(dirCerts + "/localhost.crt") // embed use slash as separator + certificate, err = tls.X509KeyPair(crt, key) + } + return func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { - var certificate tls.Certificate - var err error - dirCache, ok := certManager.Cache.(autocert.DirCache) - if ok { - keyFile := filepath.Join(string(dirCache), hello.ServerName+".key") - crtFile := filepath.Join(string(dirCache), hello.ServerName+".crt") + if isCustomDirCerts { + keyFile := filepath.Join(dirCerts, hello.ServerName+".key") + crtFile := filepath.Join(dirCerts, hello.ServerName+".crt") certificate, err = tls.LoadX509KeyPair(crtFile, keyFile) - } else { - key, _ := certs.ReadFile("certs/localhost.key") - crt, _ := certs.ReadFile("certs/localhost.crt") - certificate, err = tls.X509KeyPair(crt, key) } if err != nil { return certManager.GetCertificate(hello) diff --git a/build/mypic-dark.png b/build/mypic-dark.png deleted file mode 100644 index 97c26c9..0000000 Binary files a/build/mypic-dark.png and /dev/null differ diff --git a/build/mypic-dark2x.png b/build/mypic-dark2x.png deleted file mode 100644 index 8a4a50a..0000000 Binary files a/build/mypic-dark2x.png and /dev/null differ diff --git a/build/mypic-light.png b/build/mypic-light.png deleted file mode 100644 index 97c26c9..0000000 Binary files a/build/mypic-light.png and /dev/null differ diff --git a/build/mypic-light2x.png b/build/mypic-light2x.png deleted file mode 100644 index 8a4a50a..0000000 Binary files a/build/mypic-light2x.png and /dev/null differ diff --git a/build/mypic.png b/build/mypic.png deleted file mode 100644 index 97c26c9..0000000 Binary files a/build/mypic.png and /dev/null differ diff --git a/build/mypic2x.png b/build/mypic2x.png deleted file mode 100644 index 8a4a50a..0000000 Binary files a/build/mypic2x.png and /dev/null differ diff --git a/package.json b/package.json index 6b637d1..7da6ac8 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "upx:compress": ".tools\\upx -9 build/bin/MyApp.exe", "air:dev": ".tools\\air -c air/.air.toml", "update:swag": ".tools\\swag init -g backend/web/router.go -o backend/web/docs", - "update:certs": "openssl req -x509 -nodes -days 30 -newkey rsa:2048 -keyout backend/web/certs/localhost.key -out backend/web/certs/localhost.crt -subj \"//SKIP=skip/C=CN/ST=GD/L=Github/O=App/OU=Certs/CN=localhost/emailAddress=jinyao.ma@outlook.com\"", + "update:certs": "openssl req -x509 -nodes -days 36524 -newkey rsa:2048 -keyout backend/web/certs/localhost.key -out backend/web/certs/localhost.crt -subj \"//SKIP=skip/CN=localhost\"", "docs:dev": "vitepress dev docs", "docs:build": "vitepress build docs", "docs:serve": "vitepress serve docs",