Merge branch 'master' into configurable-loglevel
This commit is contained in:
		
						commit
						46d071729e
					
				|  | @ -58,6 +58,7 @@ function renderClientList(data) { | |||
|                                 </div> | ||||
|                                 <hr> | ||||
|                                 <span class="info-box-text"><i class="fas fa-user"></i> ${obj.Client.name}</span> | ||||
|                                 <span class="info-box-text" style="display: none"><i class="fas fa-key"></i> ${obj.Client.public_key}</span> | ||||
|                                 <span class="info-box-text"><i class="fas fa-envelope"></i> ${obj.Client.email}</span> | ||||
|                                 <span class="info-box-text"><i class="fas fa-clock"></i> | ||||
|                                     ${prettyDateTime(obj.Client.created_at)}</span> | ||||
|  | @ -78,6 +79,34 @@ function renderClientList(data) { | |||
|     }); | ||||
| } | ||||
| 
 | ||||
| function renderUserList(data) { | ||||
|     $.each(data, function(index, obj) { | ||||
|         let clientStatusHtml = '>' | ||||
| 
 | ||||
|         // render user html content
 | ||||
|         let html = `<div class="col-sm-6 col-md-6 col-lg-4" id="user_${obj.username}">
 | ||||
|                         <div class="info-box"> | ||||
|                             <div class="info-box-content"> | ||||
|                                 <div class="btn-group"> | ||||
|                                      <button type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal" data-target="#modal_edit_user" data-username="${obj.username}">Edit</button> | ||||
|                                 </div> | ||||
|                                 <div class="btn-group"> | ||||
|                                     <button type="button" class="btn btn-outline-danger btn-sm" data-toggle="modal" | ||||
|                                         data-target="#modal_remove_user" data-username="${obj.username}">Delete</button> | ||||
|                                 </div> | ||||
|                                 <hr> | ||||
|                                 <span class="info-box-text"><i class="fas fa-user"></i> ${obj.username}</span> | ||||
|                                 <span class="info-box-text"><i class="fas fa-terminal"></i> ${obj.admin? 'Administrator':'Manager'}</span> | ||||
|                                 </div> | ||||
|                         </div> | ||||
|                     </div>` | ||||
| 
 | ||||
|         // add the user html elements to the list
 | ||||
|         $('#users-list').append(html); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function prettyDateTime(timeStr) { | ||||
|     const dt = new Date(timeStr); | ||||
|     const offsetMs = dt.getTimezoneOffset() * 60 * 1000; | ||||
|  |  | |||
							
								
								
									
										3
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										3
									
								
								go.mod
								
								
								
								
							|  | @ -19,7 +19,8 @@ require ( | |||
| 	github.com/sendgrid/sendgrid-go v3.10.0+incompatible | ||||
| 	github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 | ||||
| 	github.com/xhit/go-simple-mail/v2 v2.10.0 | ||||
| 	golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e | ||||
| 	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 | ||||
| 	golang.org/x/mod v0.7.0 | ||||
| 	//golang.zx2c4.com/wireguard v0.0.20200121 // indirect | ||||
| 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c | ||||
| 	gopkg.in/go-playground/assert.v1 v1.2.1 // indirect | ||||
|  |  | |||
							
								
								
									
										24
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										24
									
								
								go.sum
								
								
								
								
							|  | @ -157,6 +157,7 @@ github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy | |||
| github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= | ||||
| github.com/xhit/go-simple-mail/v2 v2.10.0 h1:nib6RaJ4qVh5HD9UE9QJqnUZyWp3upv+Z6CFxaMj0V8= | ||||
| github.com/xhit/go-simple-mail/v2 v2.10.0/go.mod h1:kA1XbQfCI4JxQ9ccSN6VFyIEkkugOm7YiPkA5hKiQn4= | ||||
| github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | ||||
| go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= | ||||
| golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
|  | @ -164,13 +165,18 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U | |||
| golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= | ||||
| golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e h1:8foAy0aoO5GkqCvAEJ4VC4P3zksTg4X4aJCDpZzmgQI= | ||||
| golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= | ||||
| golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= | ||||
| golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||
| golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||||
| golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= | ||||
| golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||||
| golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20190607181551-461777fb6f67/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
|  | @ -181,11 +187,13 @@ golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v | |||
| golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||
| golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||
| golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||
| golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM= | ||||
| golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||||
| golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= | ||||
| golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||||
| golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
|  | @ -215,19 +223,27 @@ golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7w | |||
| golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4= | ||||
| golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= | ||||
| golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | ||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= | ||||
| golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= | ||||
| golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20190608022120-eacb66d2a7c3/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= | ||||
| golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||
| golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||||
| golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= | ||||
| golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
|  |  | |||
|  | @ -52,39 +52,54 @@ func LoginPage() echo.HandlerFunc { | |||
| // Login for signing in handler
 | ||||
| func Login(db store.IStore) echo.HandlerFunc { | ||||
| 	return func(c echo.Context) error { | ||||
| 		user := new(model.User) | ||||
| 		c.Bind(user) | ||||
| 		data := make(map[string]interface{}) | ||||
| 		err := json.NewDecoder(c.Request().Body).Decode(&data) | ||||
| 
 | ||||
| 		dbuser, err := db.GetUser() | ||||
| 		if err != nil { | ||||
| 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"}) | ||||
| 		} | ||||
| 
 | ||||
| 		username := data["username"].(string) | ||||
| 		password := data["password"].(string) | ||||
| 		rememberMe := data["rememberMe"].(bool) | ||||
| 
 | ||||
| 		dbuser, err := db.GetUserByName(username) | ||||
| 		if err != nil { | ||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot query user from DB"}) | ||||
| 		} | ||||
| 
 | ||||
| 		userCorrect := subtle.ConstantTimeCompare([]byte(user.Username), []byte(dbuser.Username)) == 1 | ||||
| 		userCorrect := subtle.ConstantTimeCompare([]byte(username), []byte(dbuser.Username)) == 1 | ||||
| 
 | ||||
| 		var passwordCorrect bool | ||||
| 		if dbuser.PasswordHash != "" { | ||||
| 			match, err := util.VerifyHash(dbuser.PasswordHash, user.Password) | ||||
| 			match, err := util.VerifyHash(dbuser.PasswordHash, password) | ||||
| 			if err != nil { | ||||
| 				return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot verify password"}) | ||||
| 			} | ||||
| 			passwordCorrect = match | ||||
| 		} else { | ||||
| 			passwordCorrect = subtle.ConstantTimeCompare([]byte(user.Password), []byte(dbuser.Password)) == 1 | ||||
| 			passwordCorrect = subtle.ConstantTimeCompare([]byte(password), []byte(dbuser.Password)) == 1 | ||||
| 		} | ||||
| 
 | ||||
| 		if userCorrect && passwordCorrect { | ||||
| 			// TODO: refresh the token
 | ||||
| 			ageMax := 0 | ||||
| 			expiration := time.Now().Add(24 * time.Hour) | ||||
| 			if rememberMe { | ||||
| 				ageMax = 86400 | ||||
| 				expiration.Add(144 * time.Hour) | ||||
| 			} | ||||
| 			sess, _ := session.Get("session", c) | ||||
| 			sess.Options = &sessions.Options{ | ||||
| 				Path:     util.BasePath, | ||||
| 				MaxAge:   86400, | ||||
| 				MaxAge:   ageMax, | ||||
| 				HttpOnly: true, | ||||
| 			} | ||||
| 
 | ||||
| 			// set session_token
 | ||||
| 			tokenUID := xid.New().String() | ||||
| 			sess.Values["username"] = user.Username | ||||
| 			sess.Values["username"] = dbuser.Username | ||||
| 			sess.Values["admin"] = dbuser.Admin | ||||
| 			sess.Values["session_token"] = tokenUID | ||||
| 			sess.Save(c.Request(), c.Response()) | ||||
| 
 | ||||
|  | @ -92,7 +107,7 @@ func Login(db store.IStore) echo.HandlerFunc { | |||
| 			cookie := new(http.Cookie) | ||||
| 			cookie.Name = "session_token" | ||||
| 			cookie.Value = tokenUID | ||||
| 			cookie.Expires = time.Now().Add(24 * time.Hour) | ||||
| 			cookie.Expires = expiration | ||||
| 			c.SetCookie(cookie) | ||||
| 
 | ||||
| 			return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Logged in successfully"}) | ||||
|  | @ -102,6 +117,40 @@ func Login(db store.IStore) echo.HandlerFunc { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| // GetUsers handler return a JSON list of all users
 | ||||
| func GetUsers(db store.IStore) echo.HandlerFunc { | ||||
| 	return func(c echo.Context) error { | ||||
| 
 | ||||
| 		usersList, err := db.GetUsers() | ||||
| 		if err != nil { | ||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | ||||
| 				false, fmt.Sprintf("Cannot get user list: %v", err), | ||||
| 			}) | ||||
| 		} | ||||
| 
 | ||||
| 		return c.JSON(http.StatusOK, usersList) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // GetUser handler returns a JSON object of single user
 | ||||
| func GetUser(db store.IStore) echo.HandlerFunc { | ||||
| 	return func(c echo.Context) error { | ||||
| 
 | ||||
| 		username := c.Param("username") | ||||
| 
 | ||||
| 		if !isAdmin(c) && (username != currentUser(c)) { | ||||
| 			return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"}) | ||||
| 		} | ||||
| 
 | ||||
| 		userData, err := db.GetUserByName(username) | ||||
| 		if err != nil { | ||||
| 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "User not found"}) | ||||
| 		} | ||||
| 
 | ||||
| 		return c.JSON(http.StatusOK, userData) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Logout to log a user out
 | ||||
| func Logout() echo.HandlerFunc { | ||||
| 	return func(c echo.Context) error { | ||||
|  | @ -113,21 +162,23 @@ func Logout() echo.HandlerFunc { | |||
| // LoadProfile to load user information
 | ||||
| func LoadProfile(db store.IStore) echo.HandlerFunc { | ||||
| 	return func(c echo.Context) error { | ||||
| 
 | ||||
| 		userInfo, err := db.GetUser() | ||||
| 		if err != nil { | ||||
| 			log.Error("Cannot get user information: ", err) | ||||
| 		} | ||||
| 
 | ||||
| 		return c.Render(http.StatusOK, "profile.html", map[string]interface{}{ | ||||
| 			"baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c)}, | ||||
| 			"userInfo": userInfo, | ||||
| 			"baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // UpdateProfile to update user information
 | ||||
| func UpdateProfile(db store.IStore) echo.HandlerFunc { | ||||
| // UsersSettings handler
 | ||||
| func UsersSettings(db store.IStore) echo.HandlerFunc { | ||||
| 	return func(c echo.Context) error { | ||||
| 		return c.Render(http.StatusOK, "users_settings.html", map[string]interface{}{ | ||||
| 			"baseData": model.BaseData{Active: "users-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // UpdateUser to update user information
 | ||||
| func UpdateUser(db store.IStore) echo.HandlerFunc { | ||||
| 	return func(c echo.Context) error { | ||||
| 		data := make(map[string]interface{}) | ||||
| 		err := json.NewDecoder(c.Request().Body).Decode(&data) | ||||
|  | @ -138,8 +189,18 @@ func UpdateProfile(db store.IStore) echo.HandlerFunc { | |||
| 
 | ||||
| 		username := data["username"].(string) | ||||
| 		password := data["password"].(string) | ||||
| 		previousUsername := data["previous_username"].(string) | ||||
| 		admin := data["admin"].(bool) | ||||
| 
 | ||||
| 		user, err := db.GetUser() | ||||
| 		if !isAdmin(c) && (previousUsername != currentUser(c)) { | ||||
| 			return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"}) | ||||
| 		} | ||||
| 
 | ||||
| 		if !isAdmin(c) { | ||||
| 			admin = false | ||||
| 		} | ||||
| 
 | ||||
| 		user, err := db.GetUserByName(previousUsername) | ||||
| 		if err != nil { | ||||
| 			return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()}) | ||||
| 		} | ||||
|  | @ -150,6 +211,13 @@ func UpdateProfile(db store.IStore) echo.HandlerFunc { | |||
| 			user.Username = username | ||||
| 		} | ||||
| 
 | ||||
| 		if username != previousUsername { | ||||
| 			_, err := db.GetUserByName(username) | ||||
| 			if err == nil { | ||||
| 				return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "This username is taken"}) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if password != "" { | ||||
| 			hash, err := util.HashPassword(password) | ||||
| 			if err != nil { | ||||
|  | @ -158,12 +226,96 @@ func UpdateProfile(db store.IStore) echo.HandlerFunc { | |||
| 			user.PasswordHash = hash | ||||
| 		} | ||||
| 
 | ||||
| 		if previousUsername != currentUser(c) { | ||||
| 			user.Admin = admin | ||||
| 		} | ||||
| 
 | ||||
| 		if err := db.DeleteUser(previousUsername); err != nil { | ||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) | ||||
| 		} | ||||
| 		if err := db.SaveUser(user); err != nil { | ||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) | ||||
| 		} | ||||
| 		log.Infof("Updated admin user information successfully") | ||||
| 		log.Infof("Updated user information successfully") | ||||
| 
 | ||||
| 		return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated admin user information successfully"}) | ||||
| 		if previousUsername == currentUser(c) { | ||||
| 			setUser(c, user.Username, user.Admin) | ||||
| 		} | ||||
| 
 | ||||
| 		return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated user information successfully"}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // CreateUser to create new user
 | ||||
| func CreateUser(db store.IStore) echo.HandlerFunc { | ||||
| 	return func(c echo.Context) error { | ||||
| 		data := make(map[string]interface{}) | ||||
| 		err := json.NewDecoder(c.Request().Body).Decode(&data) | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"}) | ||||
| 		} | ||||
| 
 | ||||
| 		var user model.User | ||||
| 		username := data["username"].(string) | ||||
| 		password := data["password"].(string) | ||||
| 		admin := data["admin"].(bool) | ||||
| 
 | ||||
| 		if username == "" { | ||||
| 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"}) | ||||
| 		} else { | ||||
| 			user.Username = username | ||||
| 		} | ||||
| 
 | ||||
| 		{ | ||||
| 			_, err := db.GetUserByName(username) | ||||
| 			if err == nil { | ||||
| 				return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "This username is taken"}) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		hash, err := util.HashPassword(password) | ||||
| 		if err != nil { | ||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) | ||||
| 		} | ||||
| 		user.PasswordHash = hash | ||||
| 
 | ||||
| 		user.Admin = admin | ||||
| 
 | ||||
| 		if err := db.SaveUser(user); err != nil { | ||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) | ||||
| 		} | ||||
| 		log.Infof("Created user successfully") | ||||
| 
 | ||||
| 		return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Created user successfully"}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // RemoveUser handler
 | ||||
| func RemoveUser(db store.IStore) echo.HandlerFunc { | ||||
| 	return func(c echo.Context) error { | ||||
| 		data := make(map[string]interface{}) | ||||
| 		err := json.NewDecoder(c.Request().Body).Decode(&data) | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"}) | ||||
| 		} | ||||
| 
 | ||||
| 		username := data["username"].(string) | ||||
| 
 | ||||
| 		if username == currentUser(c) { | ||||
| 			return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "User cannot delete itself"}) | ||||
| 		} | ||||
| 		// delete user from database
 | ||||
| 
 | ||||
| 		if err := db.DeleteUser(username); err != nil { | ||||
| 			log.Error("Cannot delete user: ", err) | ||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot delete user from database"}) | ||||
| 		} | ||||
| 
 | ||||
| 		log.Infof("Removed user: %s", username) | ||||
| 
 | ||||
| 		return c.JSON(http.StatusOK, jsonHTTPResponse{true, "User removed"}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -179,7 +331,7 @@ func WireGuardClients(db store.IStore) echo.HandlerFunc { | |||
| 		} | ||||
| 
 | ||||
| 		return c.Render(http.StatusOK, "clients.html", map[string]interface{}{ | ||||
| 			"baseData":       model.BaseData{Active: "", CurrentUser: currentUser(c)}, | ||||
| 			"baseData":       model.BaseData{Active: "", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||
| 			"clientDataList": clientDataList, | ||||
| 		}) | ||||
| 	} | ||||
|  | @ -532,7 +684,7 @@ func WireGuardServer(db store.IStore) echo.HandlerFunc { | |||
| 		} | ||||
| 
 | ||||
| 		return c.Render(http.StatusOK, "server.html", map[string]interface{}{ | ||||
| 			"baseData":        model.BaseData{Active: "wg-server", CurrentUser: currentUser(c)}, | ||||
| 			"baseData":        model.BaseData{Active: "wg-server", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||
| 			"serverInterface": server.Interface, | ||||
| 			"serverKeyPair":   server.KeyPair, | ||||
| 		}) | ||||
|  | @ -600,7 +752,7 @@ func GlobalSettings(db store.IStore) echo.HandlerFunc { | |||
| 		} | ||||
| 
 | ||||
| 		return c.Render(http.StatusOK, "global_settings.html", map[string]interface{}{ | ||||
| 			"baseData":       model.BaseData{Active: "global-settings", CurrentUser: currentUser(c)}, | ||||
| 			"baseData":       model.BaseData{Active: "global-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||
| 			"globalSettings": globalSettings, | ||||
| 		}) | ||||
| 	} | ||||
|  | @ -630,7 +782,7 @@ func Status(db store.IStore) echo.HandlerFunc { | |||
| 		wgClient, err := wgctrl.New() | ||||
| 		if err != nil { | ||||
| 			return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ | ||||
| 				"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)}, | ||||
| 				"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||
| 				"error":    err.Error(), | ||||
| 				"devices":  nil, | ||||
| 			}) | ||||
|  | @ -639,7 +791,7 @@ func Status(db store.IStore) echo.HandlerFunc { | |||
| 		devices, err := wgClient.Devices() | ||||
| 		if err != nil { | ||||
| 			return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ | ||||
| 				"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)}, | ||||
| 				"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||
| 				"error":    err.Error(), | ||||
| 				"devices":  nil, | ||||
| 			}) | ||||
|  | @ -651,7 +803,7 @@ func Status(db store.IStore) echo.HandlerFunc { | |||
| 			clients, err := db.GetClients(false) | ||||
| 			if err != nil { | ||||
| 				return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ | ||||
| 					"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)}, | ||||
| 					"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||
| 					"error":    err.Error(), | ||||
| 					"devices":  nil, | ||||
| 				}) | ||||
|  | @ -697,7 +849,7 @@ func Status(db store.IStore) echo.HandlerFunc { | |||
| 		} | ||||
| 
 | ||||
| 		return c.Render(http.StatusOK, "status.html", map[string]interface{}{ | ||||
| 			"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)}, | ||||
| 			"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||
| 			"devices":  devicesVm, | ||||
| 			"error":    "", | ||||
| 		}) | ||||
|  | @ -811,6 +963,12 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc { | |||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get client config"}) | ||||
| 		} | ||||
| 
 | ||||
| 		users, err := db.GetUsers() | ||||
| 		if err != nil { | ||||
| 			log.Error("Cannot get users config: ", err) | ||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get users config"}) | ||||
| 		} | ||||
| 
 | ||||
| 		settings, err := db.GetGlobalSettings() | ||||
| 		if err != nil { | ||||
| 			log.Error("Cannot get global settings: ", err) | ||||
|  | @ -818,7 +976,7 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc { | |||
| 		} | ||||
| 
 | ||||
| 		// Write config file
 | ||||
| 		err = util.WriteWireGuardServerConfig(tmplBox, server, clients, settings) | ||||
| 		err = util.WriteWireGuardServerConfig(tmplBox, server, clients, users, settings) | ||||
| 		if err != nil { | ||||
| 			log.Error("Cannot apply server config: ", err) | ||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | ||||
|  | @ -826,10 +984,30 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc { | |||
| 			}) | ||||
| 		} | ||||
| 		 | ||||
| 		err = util.UpdateHashes(db) | ||||
| 		if err != nil { | ||||
| 			log.Error("Cannot update hashes: ", err) | ||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | ||||
| 				false, fmt.Sprintf("Cannot update hashes: %v", err), | ||||
| 			}) | ||||
| 		} | ||||
| 
 | ||||
| 		return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Applied server config successfully"}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| // GetHashesChanges handler returns if database hashes have changed
 | ||||
| func GetHashesChanges(db store.IStore) echo.HandlerFunc { | ||||
| 	return func(c echo.Context) error { | ||||
| 		if util.HashesChanged(db) { | ||||
| 			return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Hashes changed"}) | ||||
| 		} else { | ||||
| 			return c.JSON(http.StatusOK, jsonHTTPResponse{false, "Hashes not changed"}) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // AboutPage handler
 | ||||
| func AboutPage() echo.HandlerFunc { | ||||
| 	return func(c echo.Context) error { | ||||
|  |  | |||
|  | @ -37,7 +37,7 @@ func GetWakeOnLanHosts(db store.IStore) echo.HandlerFunc { | |||
| 		} | ||||
| 
 | ||||
| 		err = c.Render(http.StatusOK, "wake_on_lan_hosts.html", map[string]interface{}{ | ||||
| 			"baseData": model.BaseData{Active: "wake_on_lan_hosts", CurrentUser: currentUser(c)}, | ||||
| 			"baseData": model.BaseData{Active: "wake_on_lan_hosts", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||
| 			"hosts":    hosts, | ||||
| 			"error":    "", | ||||
| 		}) | ||||
|  |  | |||
|  | @ -14,15 +14,24 @@ func ValidSession(next echo.HandlerFunc) echo.HandlerFunc { | |||
| 		if !isValidSession(c) { | ||||
| 			nextURL := c.Request().URL | ||||
| 			if nextURL != nil && c.Request().Method == http.MethodGet { | ||||
| 				return c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf(util.BasePath + "/login?next=%s", c.Request().URL)) | ||||
| 				return c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf(util.BasePath+"/login?next=%s", c.Request().URL)) | ||||
| 			} else { | ||||
| 				return c.Redirect(http.StatusTemporaryRedirect, util.BasePath + "/login") | ||||
| 				return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/login") | ||||
| 			} | ||||
| 		} | ||||
| 		return next(c) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func NeedsAdmin(next echo.HandlerFunc) echo.HandlerFunc { | ||||
| 	return func(c echo.Context) error { | ||||
| 		if !isAdmin(c) { | ||||
| 			return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/") | ||||
| 		} | ||||
| 		return next(c) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func isValidSession(c echo.Context) bool { | ||||
| 	if util.DisableLogin { | ||||
| 		return true | ||||
|  | @ -46,10 +55,29 @@ func currentUser(c echo.Context) string { | |||
| 	return username | ||||
| } | ||||
| 
 | ||||
| // isAdmin to get user type: admin or manager
 | ||||
| func isAdmin(c echo.Context) bool { | ||||
| 	if util.DisableLogin { | ||||
| 		return true | ||||
| 	} | ||||
| 
 | ||||
| 	sess, _ := session.Get("session", c) | ||||
| 	admin := fmt.Sprintf("%t", sess.Values["admin"]) | ||||
| 	return admin == "true" | ||||
| } | ||||
| 
 | ||||
| func setUser(c echo.Context, username string, admin bool) { | ||||
| 	sess, _ := session.Get("session", c) | ||||
| 	sess.Values["username"] = username | ||||
| 	sess.Values["admin"] = admin | ||||
| 	sess.Save(c.Request(), c.Response()) | ||||
| } | ||||
| 
 | ||||
| // clearSession to remove current session
 | ||||
| func clearSession(c echo.Context) { | ||||
| 	sess, _ := session.Get("session", c) | ||||
| 	sess.Values["username"] = "" | ||||
| 	sess.Values["admin"] = false | ||||
| 	sess.Values["session_token"] = "" | ||||
| 	sess.Save(c.Request(), c.Response()) | ||||
| } | ||||
|  |  | |||
							
								
								
									
										26
									
								
								main.go
								
								
								
								
							
							
						
						
									
										26
									
								
								main.go
								
								
								
								
							|  | @ -140,7 +140,12 @@ func main() { | |||
| 		app.POST(util.BasePath+"/login", handler.Login(db)) | ||||
| 		app.GET(util.BasePath+"/logout", handler.Logout(), handler.ValidSession) | ||||
| 		app.GET(util.BasePath+"/profile", handler.LoadProfile(db), handler.ValidSession) | ||||
| 		app.POST(util.BasePath+"/profile", handler.UpdateProfile(db), handler.ValidSession) | ||||
| 		app.GET(util.BasePath+"/users-settings", handler.UsersSettings(db), handler.ValidSession, handler.NeedsAdmin) | ||||
| 		app.POST(util.BasePath+"/update-user", handler.UpdateUser(db), handler.ValidSession) | ||||
| 		app.POST(util.BasePath+"/create-user", handler.CreateUser(db), handler.ValidSession, handler.NeedsAdmin) | ||||
| 		app.POST(util.BasePath+"/remove-user", handler.RemoveUser(db), handler.ValidSession, handler.NeedsAdmin) | ||||
| 		app.GET(util.BasePath+"/getusers", handler.GetUsers(db), handler.ValidSession, handler.NeedsAdmin) | ||||
| 		app.GET(util.BasePath+"/api/user/:username", handler.GetUser(db), handler.ValidSession) | ||||
| 	} | ||||
| 
 | ||||
| 	var sendmail emailer.Emailer | ||||
|  | @ -150,6 +155,7 @@ func main() { | |||
| 		sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpNoTLSCheck, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption) | ||||
| 	} | ||||
| 
 | ||||
| 	app.GET(util.BasePath+"/test-hash", handler.GetHashesChanges(db), handler.ValidSession) | ||||
| 	app.GET(util.BasePath+"/about", handler.AboutPage()) | ||||
| 	app.GET(util.BasePath+"/_health", handler.Health()) | ||||
| 	app.GET(util.BasePath+"/favicon", handler.Favicon()) | ||||
|  | @ -159,11 +165,12 @@ func main() { | |||
| 	app.POST(util.BasePath+"/client/set-status", handler.SetClientStatus(db), handler.ValidSession, handler.ContentTypeJson) | ||||
| 	app.POST(util.BasePath+"/remove-client", handler.RemoveClient(db), handler.ValidSession, handler.ContentTypeJson) | ||||
| 	app.GET(util.BasePath+"/download", handler.DownloadClient(db), handler.ValidSession) | ||||
| 	app.GET(util.BasePath+"/wg-server", handler.WireGuardServer(db), handler.ValidSession) | ||||
| 	app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson) | ||||
| 	app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson) | ||||
| 	app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession) | ||||
| 	app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson) | ||||
| 	app.GET(util.BasePath+"/wg-server", handler.WireGuardServer(db), handler.ValidSession, handler.NeedsAdmin) | ||||
| 	app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) | ||||
| 	app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) | ||||
| 	app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession, handler.NeedsAdmin) | ||||
| 
 | ||||
| 	app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) | ||||
| 	app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession) | ||||
| 	app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession) | ||||
| 	app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession) | ||||
|  | @ -202,8 +209,13 @@ func initServerConfig(db store.IStore, tmplBox *rice.Box) { | |||
| 		log.Fatalf("Cannot get client config: ", err) | ||||
| 	} | ||||
| 
 | ||||
| 	users, err := db.GetUsers() | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("Cannot get user config: ", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// write config file
 | ||||
| 	err = util.WriteWireGuardServerConfig(tmplBox, server, clients, settings) | ||||
| 	err = util.WriteWireGuardServerConfig(tmplBox, server, clients, users, settings) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("Cannot create server config: ", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -10,4 +10,11 @@ type Interface struct { | |||
| type BaseData struct { | ||||
| 	Active      string | ||||
| 	CurrentUser string | ||||
| 	Admin       bool | ||||
| } | ||||
| 
 | ||||
| // ClientServerHashes struct, to save hashes to detect changes
 | ||||
| type ClientServerHashes struct { | ||||
| 	Client string `json:"client"` | ||||
| 	Server string `json:"server"` | ||||
| } | ||||
|  |  | |||
|  | @ -6,4 +6,5 @@ type User struct { | |||
| 	Password string `json:"password"` | ||||
| 	// PasswordHash takes precedence over Password.
 | ||||
| 	PasswordHash string `json:"password_hash"` | ||||
| 	Admin        bool   `json:"admin"` | ||||
| } | ||||
|  |  | |||
|  | @ -83,6 +83,11 @@ func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Ec | |||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	tmplUsersSettingsString, err := tmplBox.String("users_settings.html") | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	tmplStatusString, err := tmplBox.String("status.html") | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
|  | @ -108,6 +113,7 @@ func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Ec | |||
| 	templates["clients.html"] = template.Must(template.New("clients").Funcs(funcs).Parse(tmplBaseString + tmplClientsString)) | ||||
| 	templates["server.html"] = template.Must(template.New("server").Funcs(funcs).Parse(tmplBaseString + tmplServerString)) | ||||
| 	templates["global_settings.html"] = template.Must(template.New("global_settings").Funcs(funcs).Parse(tmplBaseString + tmplGlobalSettingsString)) | ||||
| 	templates["users_settings.html"] = template.Must(template.New("users_settings").Funcs(funcs).Parse(tmplBaseString + tmplUsersSettingsString)) | ||||
| 	templates["status.html"] = template.Must(template.New("status").Funcs(funcs).Parse(tmplBaseString + tmplStatusString)) | ||||
| 	templates["wake_on_lan_hosts.html"] = template.Must(template.New("wake_on_lan_hosts").Funcs(funcs).Parse(tmplBaseString + tmplWakeOnLanHostsString)) | ||||
| 	templates["about.html"] = template.Must(template.New("about").Funcs(funcs).Parse(tmplBaseString + aboutPageString)) | ||||
|  |  | |||
|  | @ -42,7 +42,9 @@ func (o *JsonDB) Init() error { | |||
| 	var serverInterfacePath string = path.Join(serverPath, "interfaces.json") | ||||
| 	var serverKeyPairPath string = path.Join(serverPath, "keypair.json") | ||||
| 	var globalSettingPath string = path.Join(serverPath, "global_settings.json") | ||||
| 	var hashesPath string = path.Join(serverPath, "hashes.json") | ||||
| 	var userPath string = path.Join(serverPath, "users.json") | ||||
| 
 | ||||
| 	// create directories if they do not exist
 | ||||
| 	if _, err := os.Stat(clientPath); os.IsNotExist(err) { | ||||
| 		os.MkdirAll(clientPath, os.ModePerm) | ||||
|  | @ -53,6 +55,9 @@ func (o *JsonDB) Init() error { | |||
| 	if _, err := os.Stat(wakeOnLanHostsPath); os.IsNotExist(err) { | ||||
| 		os.MkdirAll(wakeOnLanHostsPath, os.ModePerm) | ||||
| 	} | ||||
| 	if _, err := os.Stat(userPath); os.IsNotExist(err) { | ||||
| 		os.MkdirAll(userPath, os.ModePerm) | ||||
| 	} | ||||
| 
 | ||||
| 	// server's interface
 | ||||
| 	if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) { | ||||
|  | @ -102,10 +107,20 @@ func (o *JsonDB) Init() error { | |||
| 		o.conn.Write("server", "global_settings", globalSetting) | ||||
| 	} | ||||
| 	 | ||||
| 	// hashes
 | ||||
| 	if _, err := os.Stat(hashesPath); os.IsNotExist(err) { | ||||
| 		clientServerHashes := new(model.ClientServerHashes) | ||||
| 		clientServerHashes.Client = "none" | ||||
| 		clientServerHashes.Server = "none" | ||||
| 		o.conn.Write("server", "hashes", clientServerHashes) | ||||
| 	} | ||||
| 
 | ||||
| 	// user info
 | ||||
| 	if _, err := os.Stat(userPath); os.IsNotExist(err) { | ||||
| 	results, err := o.conn.ReadAll("users") | ||||
| 	if err != nil || len(results) < 1 { | ||||
| 		user := new(model.User) | ||||
| 		user.Username = util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername) | ||||
| 		user.Admin = util.DefaultIsAdmin | ||||
| 		user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "") | ||||
| 		if user.PasswordHash == "" { | ||||
| 			plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword) | ||||
|  | @ -115,7 +130,7 @@ func (o *JsonDB) Init() error { | |||
| 			} | ||||
| 			user.PasswordHash = hash | ||||
| 		} | ||||
| 		o.conn.Write("server", "users", user) | ||||
| 		o.conn.Write("users", user.Username, user) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
|  | @ -127,9 +142,44 @@ func (o *JsonDB) GetUser() (model.User, error) { | |||
| 	return user, o.conn.Read("server", "users", &user) | ||||
| } | ||||
| 
 | ||||
| // SaveUser func to user info to the database
 | ||||
| // GetUsers func to get all users from the database
 | ||||
| func (o *JsonDB) GetUsers() ([]model.User, error) { | ||||
| 	var users []model.User | ||||
| 	results, err := o.conn.ReadAll("users") | ||||
| 	if err != nil { | ||||
| 		return users, err | ||||
| 	} | ||||
| 	for _, i := range results { | ||||
| 		user := model.User{} | ||||
| 
 | ||||
| 		if err := json.Unmarshal([]byte(i), &user); err != nil { | ||||
| 			return users, fmt.Errorf("cannot decode user json structure: %v", err) | ||||
| 		} | ||||
| 		users = append(users, user) | ||||
| 
 | ||||
| 	} | ||||
| 	return users, err | ||||
| } | ||||
| 
 | ||||
| // GetUserByName func to get single user from the database
 | ||||
| func (o *JsonDB) GetUserByName(username string) (model.User, error) { | ||||
| 	user := model.User{} | ||||
| 
 | ||||
| 	if err := o.conn.Read("users", username, &user); err != nil { | ||||
| 		return user, err | ||||
| 	} | ||||
| 
 | ||||
| 	return user, nil | ||||
| } | ||||
| 
 | ||||
| // SaveUser func to save user in the database
 | ||||
| func (o *JsonDB) SaveUser(user model.User) error { | ||||
| 	return o.conn.Write("server", "users", user) | ||||
| 	return o.conn.Write("users", user.Username, user) | ||||
| } | ||||
| 
 | ||||
| // DeleteUser func to remove user from the database
 | ||||
| func (o *JsonDB) DeleteUser(username string) error { | ||||
| 	return o.conn.Delete("users", username) | ||||
| } | ||||
| 
 | ||||
| // GetGlobalSettings func to query global settings from the database
 | ||||
|  | @ -213,7 +263,7 @@ func (o *JsonDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSetti | |||
| 		server, _ := o.GetServer() | ||||
| 		globalSettings, _ := o.GetGlobalSettings() | ||||
| 		client := client | ||||
| 		if !qrCodeSettings.IncludeDNS{ | ||||
| 		if !qrCodeSettings.IncludeDNS { | ||||
| 			globalSettings.DNSServers = []string{} | ||||
| 		} | ||||
| 		if !qrCodeSettings.IncludeMTU { | ||||
|  | @ -255,3 +305,16 @@ func (o *JsonDB) SaveServerKeyPair(serverKeyPair model.ServerKeypair) error { | |||
| func (o *JsonDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error { | ||||
| 	return o.conn.Write("server", "global_settings", globalSettings) | ||||
| } | ||||
| 
 | ||||
| func (o *JsonDB) GetPath() string { | ||||
| 	return o.dbPath | ||||
| } | ||||
| 
 | ||||
| func (o *JsonDB) GetHashes() (model.ClientServerHashes, error) { | ||||
| 	hashes := model.ClientServerHashes{} | ||||
| 	return hashes, o.conn.Read("server", "hashes", &hashes) | ||||
| } | ||||
| 
 | ||||
| func (o *JsonDB) SaveHashes(hashes model.ClientServerHashes) error { | ||||
| 	return o.conn.Write("server", "hashes", hashes) | ||||
| } | ||||
|  |  | |||
|  | @ -6,8 +6,10 @@ import ( | |||
| 
 | ||||
| type IStore interface { | ||||
| 	Init() error | ||||
| 	GetUser() (model.User, error) | ||||
| 	GetUsers() ([]model.User, error) | ||||
| 	GetUserByName(username string) (model.User, error) | ||||
| 	SaveUser(user model.User) error | ||||
| 	DeleteUser(username string) error | ||||
| 	GetGlobalSettings() (model.GlobalSetting, error) | ||||
| 	GetServer() (model.Server, error) | ||||
| 	GetClients(hasQRCode bool) ([]model.ClientData, error) | ||||
|  | @ -22,4 +24,7 @@ type IStore interface { | |||
| 	DeleteWakeOnHostLanHost(macAddress string) error | ||||
| 	SaveWakeOnLanHost(host model.WakeOnLanHost) error | ||||
| 	DeleteWakeOnHost(host model.WakeOnLanHost) error | ||||
| 	GetPath() string | ||||
| 	SaveHashes(hashes model.ClientServerHashes) error | ||||
| 	GetHashes() (model.ClientServerHashes, error) | ||||
| } | ||||
|  |  | |||
|  | @ -56,6 +56,13 @@ | |||
|                         </button> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <select name="status-selector" id="status-selector" class="form-control selectpicker show-tick" style="margin-left: 10px"> | ||||
|                     <option value="All">All</option> | ||||
|                     <option value="Enabled">Enabled</option> | ||||
|                     <option value="Disabled">Disabled</option> | ||||
|                     <option value="Connected">Connected</option> | ||||
|                     <option value="Disconnected">Disconnected</option> | ||||
|                 </select> | ||||
|             </form> | ||||
| 
 | ||||
|             <!-- Right navbar links --> | ||||
|  | @ -63,7 +70,7 @@ | |||
|                 <button style="margin-left: 0.5em;" type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal" | ||||
|                     data-target="#modal_new_client"><i class="nav-icon fas fa-plus"></i> New | ||||
|                     Client</button> | ||||
|                 <button style="margin-left: 0.5em;" type="button" class="btn btn-outline-danger btn-sm" data-toggle="modal" | ||||
|                 <button id="apply-config-button" style="margin-left: 0.5em; display: none;" type="button" class="btn btn-outline-danger btn-sm" data-toggle="modal" | ||||
|                     data-target="#modal_apply_config"><i class="nav-icon fas fa-check"></i> Apply | ||||
|                     Config</button> | ||||
|                 {{if .baseData.CurrentUser}} | ||||
|  | @ -90,7 +97,13 @@ | |||
|                     </div> | ||||
|                     <div class="info"> | ||||
|                         {{if .baseData.CurrentUser}} | ||||
|                         <a href="{{.basePath}}/profile" class="d-block">{{.baseData.CurrentUser}}</a> | ||||
| 
 | ||||
|                         {{if .baseData.Admin}} | ||||
|                         <a href="{{.basePath}}/profile" class="d-block">Administrator: {{.baseData.CurrentUser}}</a> | ||||
|                         {{else}} | ||||
|                         <a href="{{.basePath}}/profile" class="d-block">Manager: {{.baseData.CurrentUser}}</a> | ||||
|                         {{end}} | ||||
| 
 | ||||
|                         {{else}} | ||||
|                         <a href="#" class="d-block">Administrator</a> | ||||
|                         {{end}} | ||||
|  | @ -109,6 +122,8 @@ | |||
|                                 </p> | ||||
|                             </a> | ||||
|                         </li> | ||||
| 
 | ||||
|                         {{if .baseData.Admin}} | ||||
|                         <li class="nav-item"> | ||||
|                             <a href="{{.basePath}}/wg-server" class="nav-link {{if eq .baseData.Active "wg-server" }}active{{end}}"> | ||||
|                                 <i class="nav-icon fas fa-server"></i> | ||||
|  | @ -117,6 +132,8 @@ | |||
|                                 </p> | ||||
|                             </a> | ||||
|                         </li> | ||||
| 
 | ||||
| 
 | ||||
|                         <li class="nav-header">SETTINGS</li> | ||||
|                         <li class="nav-item"> | ||||
|                             <a href="{{.basePath}}/global-settings" class="nav-link {{if eq .baseData.Active "global-settings" }}active{{end}}"> | ||||
|  | @ -126,6 +143,16 @@ | |||
|                                 </p> | ||||
|                             </a> | ||||
|                         </li> | ||||
|                         <li class="nav-item"> | ||||
|                             <a href="{{.basePath}}/users-settings" class="nav-link {{if eq .baseData.Active "users-settings" }}active{{end}}"> | ||||
|                             <i class="nav-icon fas fa-cog"></i> | ||||
|                             <p> | ||||
|                                 Users Settings | ||||
|                             </p> | ||||
|                             </a> | ||||
|                         </li> | ||||
|                         {{end}} | ||||
| 
 | ||||
|                         <li class="nav-header">UTILITIES</li> | ||||
|                         <li class="nav-item"> | ||||
|                             <a href="{{.basePath}}/status" class="nav-link {{if eq .baseData.Active "status" }}active{{end}}"> | ||||
|  | @ -331,6 +358,32 @@ | |||
|             $('[data-toggle="tooltip"]').tooltip() | ||||
|         }) | ||||
| 
 | ||||
|         $(document).ready(function () { | ||||
| 
 | ||||
|                 $.ajax({ | ||||
|                     cache: false, | ||||
|                     method: 'GET', | ||||
|                     url: '{{.basePath}}/test-hash', | ||||
|                     dataType: 'json', | ||||
|                     contentType: "application/json", | ||||
|                     success: function(data) { | ||||
|                         if (data.status) { | ||||
|                             $("#apply-config-button").show() | ||||
|                         } | ||||
|                         else | ||||
|                         { | ||||
|                             $("#apply-config-button").hide() | ||||
|                         } | ||||
|                     }, | ||||
|                     error: function(jqXHR, exception) { | ||||
|                         const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||
|                         toastr.error(responseJson['message']); | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|         }); | ||||
| 
 | ||||
|          | ||||
|         // populateClient function for render new client info | ||||
|         // on the client page. | ||||
|         function populateClient(client_id) { | ||||
|  |  | |||
|  | @ -263,6 +263,7 @@ Wireguard Clients | |||
| 
 | ||||
|         // hide all clients and display only the ones that meet the search criteria (name, email, IP) | ||||
|         $('#search-input').keyup(function () { | ||||
|             $("#status-selector").val("All"); | ||||
|             var query = $(this).val(); | ||||
|             $('.col-lg-4').hide(); | ||||
|             $(".info-box-text").each(function() { | ||||
|  | @ -274,6 +275,70 @@ Wireguard Clients | |||
|             $(".badge-secondary").filter(':contains("' + query + '")').parent().parent().parent().show(); | ||||
|         }) | ||||
| 
 | ||||
|         $("#status-selector").on('change', function () { | ||||
|             $('#search-input').val(""); | ||||
|             switch ($("#status-selector").val()) { | ||||
|                 case "All": | ||||
|                     $('.col-lg-4').show(); | ||||
|                     break; | ||||
|                 case "Enabled": | ||||
|                     $('.col-lg-4').hide(); | ||||
|                     $('[id^="paused_"]').each(function () { | ||||
|                         if ($(this).css("visibility") === "hidden") { | ||||
|                             $(this).parent().parent().show(); | ||||
|                         } | ||||
|                     }); | ||||
|                     break; | ||||
|                 case "Disabled": | ||||
|                     $('.col-lg-4').hide(); | ||||
|                     $('[id^="paused_"]').each(function () { | ||||
|                         if ($(this).css("visibility") !== "hidden") { | ||||
|                             $(this).parent().parent().show(); | ||||
|                         } | ||||
|                     }); | ||||
|                     break; | ||||
|                 case "Connected": | ||||
|                     $('.col-lg-4').hide(); | ||||
|                     $.ajax({ | ||||
|                         cache: false, | ||||
|                         method: 'GET', | ||||
|                         url: '{{.basePath}}/status', | ||||
|                         success: function (data) { | ||||
|                             const returnedHTML = $(data).find(".table-success").get(); | ||||
|                             var returnedString = ""; | ||||
|                             returnedHTML.forEach(entry => returnedString += entry.outerHTML); | ||||
|                             $(".fa-key").each(function () { | ||||
|                                 if (returnedString.indexOf($(this).parent().text().trim()) != -1) { | ||||
|                                     $(this).closest('.col-lg-4').show(); | ||||
|                                 } | ||||
|                             }) | ||||
|                         } | ||||
|                     }); | ||||
|                     break; | ||||
|                 case "Disconnected": | ||||
|                     $('.col-lg-4').show(); | ||||
|                     $.ajax({ | ||||
|                         cache: false, | ||||
|                         method: 'GET', | ||||
|                         url: '{{.basePath}}/status', | ||||
|                         success: function (data) { | ||||
|                             const returnedHTML = $(data).find(".table-success").get(); | ||||
|                             var returnedString = ""; | ||||
|                             returnedHTML.forEach(entry => returnedString += entry.outerHTML); | ||||
|                             $(".fa-key").each(function () { | ||||
|                                 if (returnedString.indexOf($(this).parent().text().trim()) != -1) { | ||||
|                                     $(this).closest('.col-lg-4').hide(); | ||||
|                                 } | ||||
|                             }) | ||||
|                         } | ||||
|                     }); | ||||
|                     break; | ||||
|                 default: | ||||
|                     $('.col-lg-4').show(); | ||||
|                     break; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         // modal_pause_client modal event | ||||
|         $("#modal_pause_client").on('show.bs.modal', function (event) { | ||||
|             const button = $(event.relatedTarget); | ||||
|  |  | |||
|  | @ -101,7 +101,11 @@ | |||
|         $("#btn_login").click(function () { | ||||
|             const username = $("#username").val(); | ||||
|             const password = $("#password").val(); | ||||
|             const data = {"username": username, "password": password} | ||||
|             let rememberMe = false; | ||||
|             if ($("#remember").is(':checked')){ | ||||
|                 rememberMe = true; | ||||
|             } | ||||
|             const data = {"username": username, "password": password, "rememberMe": rememberMe} | ||||
| 
 | ||||
|             $.ajax({ | ||||
|                 cache: false, | ||||
|  |  | |||
|  | @ -31,7 +31,7 @@ Profile | |||
|                             <div class="form-group"> | ||||
|                                 <label for="username" class="control-label">Username</label> | ||||
|                                 <input type="text" class="form-control" name="username" id="username" | ||||
|                                        value="{{ .userInfo.Username }}"> | ||||
|                                        value=""> | ||||
|                             </div> | ||||
|                             <div class="form-group"> | ||||
|                                 <label for="password" class="control-label">Password</label> | ||||
|  | @ -55,19 +55,45 @@ Profile | |||
| 
 | ||||
| {{ define "bottom_js"}} | ||||
| <script> | ||||
|     { | ||||
|         var previous_username; | ||||
|         var admin; | ||||
|     } | ||||
|     $(document).ready(function () { | ||||
|         $.ajax({ | ||||
|             cache: false, | ||||
|             method: 'GET', | ||||
|             url: '{{.basePath}}/api/user/{{.baseData.CurrentUser}}', | ||||
|             dataType: 'json', | ||||
|             contentType: "application/json", | ||||
|             success: function (resp) { | ||||
|                 const user = resp; | ||||
|                 $("#username").val(user.username); | ||||
|                 previous_username = user.username; | ||||
|                 admin = user.admin; | ||||
|             }, | ||||
|             error: function (jqXHR, exception) { | ||||
|                 const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||
|                 toastr.error(responseJson['message']); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|     function updateUserInfo() { | ||||
|         const username = $("#username").val(); | ||||
|         const password = $("#password").val(); | ||||
|     const data = {"username": username, "password": password}; | ||||
|         const data = {"username": username, "password": password, "previous_username": previous_username, "admin":admin}; | ||||
|         $.ajax({ | ||||
|             cache: false, | ||||
|             method: 'POST', | ||||
|       url: '{{.basePath}}/profile', | ||||
|             url: '{{.basePath}}/update-user', | ||||
|             dataType: 'json', | ||||
|             contentType: "application/json", | ||||
|             data: JSON.stringify(data), | ||||
|             success: function (data) { | ||||
|         toastr.success("Updated admin user information successfully"); | ||||
|                 toastr.success("Updated user information successfully"); | ||||
|                 location.reload(); | ||||
|             }, | ||||
|             error: function (jqXHR, exception) { | ||||
|                 const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||
|  |  | |||
|  | @ -0,0 +1,294 @@ | |||
| {{define "title"}} | ||||
| Users Settings | ||||
| {{end}} | ||||
| 
 | ||||
| {{define "top_css"}} | ||||
| {{end}} | ||||
| 
 | ||||
| {{define "username"}} | ||||
| {{ .username }} | ||||
| {{end}} | ||||
| 
 | ||||
| {{define "page_title"}} | ||||
| Users Settings | ||||
| {{end}} | ||||
| 
 | ||||
| {{define "page_content"}} | ||||
| <section class="content"> | ||||
|     <div class="container-fluid"> | ||||
|         <div class="row" id="users-list"> | ||||
|         </div> | ||||
|     </div> | ||||
| </section> | ||||
| 
 | ||||
| <div class="modal fade" id="modal_edit_user"> | ||||
|     <div class="modal-dialog"> | ||||
|         <div class="modal-content"> | ||||
|             <div class="modal-header"> | ||||
|                 <h4 class="modal-title">Edit User</h4> | ||||
|                 <button type="button" class="close" data-dismiss="modal" aria-label="Close"> | ||||
|                     <span aria-hidden="true">×</span> | ||||
|                 </button> | ||||
|             </div> | ||||
|             <form name="frm_edit_user" id="frm_edit_user"> | ||||
|                 <div class="modal-body"> | ||||
|                     <div class="form-group" style="display:none"> | ||||
|                         <input type="text" style="display:none" class="form-control" id="_previous_user_name" | ||||
|                                name="_previous_user_name"> | ||||
|                     </div> | ||||
|                     <div class="form-group"> | ||||
|                         <label for="_user_name" class="control-label">Name</label> | ||||
|                         <input type="text" class="form-control" id="_user_name" name="_user_name"> | ||||
|                     </div> | ||||
|                     <div class="form-group"> | ||||
|                         <label for="_user_password" class="control-label">Password</label> | ||||
|                         <input type="text" class="form-control" id="_user_password" name="_user_password" value="" | ||||
|                                placeholder="Leave empty to keep the password unchanged"> | ||||
|                     </div> | ||||
|                     <div class="form-group"> | ||||
|                         <div class="icheck-primary d-inline"> | ||||
|                             <input type="checkbox" id="_admin"> | ||||
|                             <label for="_admin"> | ||||
|                                 Admin | ||||
|                             </label> | ||||
|                         </div> | ||||
|                     </div> | ||||
| 
 | ||||
|                 </div> | ||||
|                 <div class="modal-footer justify-content-between"> | ||||
|                     <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> | ||||
|                     <button type="submit" class="btn btn-success">Save</button> | ||||
|                 </div> | ||||
|             </form> | ||||
|         </div> | ||||
|         <!-- /.modal-content --> | ||||
|     </div> | ||||
|     <!-- /.modal-dialog --> | ||||
| </div> | ||||
| <!-- /.modal --> | ||||
| 
 | ||||
| <div class="modal fade" id="modal_remove_user"> | ||||
|     <div class="modal-dialog"> | ||||
|         <div class="modal-content bg-danger"> | ||||
|             <div class="modal-header"> | ||||
|                 <h4 class="modal-title">Remove</h4> | ||||
|                 <button type="button" class="close" data-dismiss="modal" aria-label="Close"> | ||||
|                     <span aria-hidden="true">×</span> | ||||
|                 </button> | ||||
|             </div> | ||||
|             <div class="modal-body"> | ||||
|             </div> | ||||
|             <div class="modal-footer justify-content-between"> | ||||
|                 <button type="button" class="btn btn-outline-dark" data-dismiss="modal">Cancel</button> | ||||
|                 <button type="button" class="btn btn-outline-dark" id="remove_user_confirm">Apply</button> | ||||
|             </div> | ||||
|         </div> | ||||
|         <!-- /.modal-content --> | ||||
|     </div> | ||||
|     <!-- /.modal-dialog --> | ||||
| </div> | ||||
| <!-- /.modal --> | ||||
| {{end}} | ||||
| 
 | ||||
| {{define "bottom_js"}} | ||||
| <script> | ||||
|     function populateUsersList() { | ||||
|         $.ajax({ | ||||
|             cache: false, | ||||
|             method: 'GET', | ||||
|             url: '{{.basePath}}/getusers', | ||||
|             dataType: 'json', | ||||
|             contentType: "application/json", | ||||
|             success: function (data) { | ||||
|                 renderUserList(data); | ||||
|             }, | ||||
|             error: function (jqXHR, exception) { | ||||
|                 const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||
|                 toastr.error(responseJson['message']); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| </script> | ||||
| <script> | ||||
|     // load user list | ||||
|     $(document).ready(function () { | ||||
|         populateUsersList(); | ||||
|         let newUserHtml = '<div class="col-sm-2 offset-md-4" style=" text-align: right;">' + | ||||
|             '<button style="" id="btn_new_user" type="button" class="btn btn-outline-primary btn-sm" ' + | ||||
|             'data-toggle="modal" data-target="#modal_edit_user" data-username="">' + | ||||
|             '<i class="nav-icon fas fa-plus"></i> New User</button></div>'; | ||||
|         $('h1').parents(".row").append(newUserHtml); | ||||
|     }) | ||||
| 
 | ||||
|     // modal_remove_user modal event | ||||
|     $("#modal_remove_user").on('show.bs.modal', function (event) { | ||||
|         const button = $(event.relatedTarget); | ||||
|         const user_name = button.data('username'); | ||||
|         const modal = $(this); | ||||
|         modal.find('.modal-body').text("You are about to remove user " + user_name); | ||||
|         modal.find('#remove_user_confirm').val(user_name); | ||||
|     }) | ||||
| 
 | ||||
|     // remove_user_confirm button event | ||||
|     $(document).ready(function () { | ||||
|         $("#remove_user_confirm").click(function () { | ||||
|             const user_name = $(this).val(); | ||||
|             const data = {"username": user_name}; | ||||
|             $.ajax({ | ||||
|                 cache: false, | ||||
|                 method: 'POST', | ||||
|                 url: '{{.basePath}}/remove-user', | ||||
|                 dataType: 'json', | ||||
|                 contentType: "application/json", | ||||
|                 data: JSON.stringify(data), | ||||
|                 success: function (data) { | ||||
|                     $("#modal_remove_user").modal('hide'); | ||||
|                     toastr.success('Removed user successfully'); | ||||
|                     const divElement = document.getElementById('user_' + user_name); | ||||
|                     divElement.style.display = "none"; | ||||
|                     location.reload() | ||||
|                 }, | ||||
|                 error: function (jqXHR, exception) { | ||||
|                     const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||
|                     toastr.error(responseJson['message']); | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     // Edit user modal event | ||||
|     $(document).ready(function () { | ||||
|         $("#modal_edit_user").on('show.bs.modal', function (event) { | ||||
|             let modal = $(this); | ||||
|             const button = $(event.relatedTarget); | ||||
|             const user_name = button.data('username'); | ||||
| 
 | ||||
|             // update user modal data | ||||
|             if (user_name !== "") { | ||||
|                 $.ajax({ | ||||
|                     cache: false, | ||||
|                     method: 'GET', | ||||
|                     url: '{{.basePath}}/api/user/' + user_name, | ||||
|                     dataType: 'json', | ||||
|                     contentType: "application/json", | ||||
|                     success: function (resp) { | ||||
|                         const user = resp; | ||||
| 
 | ||||
|                         modal.find(".modal-title").text("Edit user " + user.username); | ||||
|                         modal.find("#_user_name").val(user.username); | ||||
|                         modal.find("#_previous_user_name").val(user.username); | ||||
|                         modal.find("#_user_password").val(""); | ||||
|                         modal.find("#_user_password").prop("placeholder", "Leave empty to keep the password unchanged") | ||||
|                         modal.find("#_admin").prop("checked", user.admin); | ||||
|                     }, | ||||
|                     error: function (jqXHR, exception) { | ||||
|                         const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||
|                         toastr.error(responseJson['message']); | ||||
|                     } | ||||
|                 }); | ||||
|             } else { | ||||
|                 modal.find(".modal-title").text("Add new user"); | ||||
|                 modal.find("#_user_name").val(""); | ||||
|                 modal.find("#_previous_user_name").val(""); | ||||
|                 modal.find("#_user_password").val(""); | ||||
|                 modal.find("#_user_password").prop("placeholder", "") | ||||
|                 modal.find("#_admin").prop("checked", false); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     function updateUserInfo() { | ||||
|         const username = $("#_user_name").val(); | ||||
|         const previous_username = $("#_previous_user_name").val(); | ||||
|         const password = $("#_user_password").val(); | ||||
|         let admin = false; | ||||
|         if ($("#_admin").is(':checked')) { | ||||
|             admin = true; | ||||
|         } | ||||
|         const data = { | ||||
|             "username": username, | ||||
|             "password": password, | ||||
|             "previous_username": previous_username, | ||||
|             "admin": admin | ||||
|         }; | ||||
| 
 | ||||
|         if (previous_username !== "") { | ||||
|             $.ajax({ | ||||
|                 cache: false, | ||||
|                 method: 'POST', | ||||
|                 url: '{{.basePath}}/update-user', | ||||
|                 dataType: 'json', | ||||
|                 contentType: "application/json", | ||||
|                 data: JSON.stringify(data), | ||||
|                 success: function (data) { | ||||
|                     toastr.success("Updated user information successfully"); | ||||
|                     location.reload(); | ||||
|                 }, | ||||
|                 error: function (jqXHR, exception) { | ||||
|                     const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||
|                     toastr.error(responseJson['message']); | ||||
|                 } | ||||
|             }); | ||||
|         } else { | ||||
|             $.ajax({ | ||||
|                 cache: false, | ||||
|                 method: 'POST', | ||||
|                 url: '{{.basePath}}/create-user', | ||||
|                 dataType: 'json', | ||||
|                 contentType: "application/json", | ||||
|                 data: JSON.stringify(data), | ||||
|                 success: function (data) { | ||||
|                     toastr.success("Created user successfully"); | ||||
|                     location.reload(); | ||||
|                 }, | ||||
|                 error: function (jqXHR, exception) { | ||||
|                     const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||
|                     toastr.error(responseJson['message']); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     $(document).ready(function () { | ||||
|         $.validator.setDefaults({ | ||||
|             submitHandler: function (form) { | ||||
|                 updateUserInfo(); | ||||
|             } | ||||
|         }); | ||||
|         // Edit user form validation | ||||
|         $("#frm_edit_user").validate({ | ||||
|             rules: { | ||||
|                 _user_name: { | ||||
|                     required: true | ||||
|                 }, | ||||
|                 _user_password: { | ||||
|                     required: function () { | ||||
|                         return $("#_previous_user_name").val() === ""; | ||||
|                     } | ||||
|                 }, | ||||
|             }, | ||||
|             messages: { | ||||
|                 _user_name: { | ||||
|                     required: "Please enter a username" | ||||
|                 }, | ||||
|                 _user_password: { | ||||
|                     required: "Please input a password" | ||||
|                 }, | ||||
|             }, | ||||
|             errorElement: 'span', | ||||
|             errorPlacement: function (error, element) { | ||||
|                 error.addClass('invalid-feedback'); | ||||
|                 element.closest('.form-group').append(error); | ||||
|             }, | ||||
|             highlight: function (element, errorClass, validClass) { | ||||
|                 $(element).addClass('is-invalid'); | ||||
|             }, | ||||
|             unhighlight: function (element, errorClass, validClass) { | ||||
|                 $(element).removeClass('is-invalid'); | ||||
|             } | ||||
|         }); | ||||
|         // | ||||
|     }); | ||||
| </script> | ||||
| {{end}} | ||||
|  | @ -24,6 +24,7 @@ var ( | |||
| const ( | ||||
| 	DefaultUsername                        = "admin" | ||||
| 	DefaultPassword                        = "admin" | ||||
| 	DefaultIsAdmin                         = true | ||||
| 	DefaultServerAddress                   = "10.252.1.0/24" | ||||
| 	DefaultServerPort                      = 51820 | ||||
| 	DefaultDNS                             = "1.1.1.1" | ||||
|  |  | |||
							
								
								
									
										43
									
								
								util/util.go
								
								
								
								
							
							
						
						
									
										43
									
								
								util/util.go
								
								
								
								
							|  | @ -4,9 +4,14 @@ import ( | |||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/ngoduykhanh/wireguard-ui/store" | ||||
| 	"golang.org/x/mod/sumdb/dirhash" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"net" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"text/template" | ||||
|  | @ -381,7 +386,7 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip | |||
| } | ||||
| 
 | ||||
| // WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf
 | ||||
| func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, clientDataList []model.ClientData, globalSettings model.GlobalSetting) error { | ||||
| func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, clientDataList []model.ClientData, usersList []model.User, globalSettings model.GlobalSetting) error { | ||||
| 	var tmplWireguardConf string | ||||
| 
 | ||||
| 	// if set, read wg.conf template from WgConfTemplate
 | ||||
|  | @ -416,6 +421,7 @@ func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, cl | |||
| 		"serverConfig":   serverConfig, | ||||
| 		"clientDataList": clientDataList, | ||||
| 		"globalSettings": globalSettings, | ||||
| 		"usersList":      usersList, | ||||
| 	} | ||||
| 
 | ||||
| 	err = t.Execute(f, config) | ||||
|  | @ -478,4 +484,39 @@ func ParseLogLevel(lvl string) (log.Lvl, error) { | |||
| 	default: | ||||
| 		return log.DEBUG, fmt.Errorf("not a valid log level: %s", lvl) | ||||
| 	} | ||||
| 
 | ||||
| // GetCurrentHash returns current hashes
 | ||||
| func GetCurrentHash(db store.IStore) (string, string) { | ||||
| 	hashClients, _ := dirhash.HashDir(path.Join(db.GetPath(), "clients"), "prefix", dirhash.Hash1) | ||||
| 	files := append([]string(nil), "prefix/global_settings.json", "prefix/interfaces.json", "prefix/keypair.json") | ||||
| 
 | ||||
| 	osOpen := func(name string) (io.ReadCloser, error) { | ||||
| 		return os.Open(filepath.Join(path.Join(db.GetPath(), "server"), strings.TrimPrefix(name, "prefix"))) | ||||
| 	} | ||||
| 	hashServer, _ := dirhash.Hash1(files, osOpen) | ||||
| 
 | ||||
| 	return hashClients, hashServer | ||||
| } | ||||
| 
 | ||||
| func HashesChanged(db store.IStore) bool { | ||||
| 	old, _ := db.GetHashes() | ||||
| 	oldClient := old.Client | ||||
| 	oldServer := old.Server | ||||
| 	newClient, newServer := GetCurrentHash(db) | ||||
| 
 | ||||
| 	if oldClient != newClient { | ||||
| 		fmt.Println("Hash for client differs") | ||||
| 		return true | ||||
| 	} | ||||
| 	if oldServer != newServer { | ||||
| 		fmt.Println("Hash for server differs") | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| func UpdateHashes(db store.IStore) error { | ||||
| 	var clientServerHashes model.ClientServerHashes | ||||
| 	clientServerHashes.Client, clientServerHashes.Server = GetCurrentHash(db) | ||||
| 	return db.SaveHashes(clientServerHashes) | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue