introdcution of typesafe ids.
This commit is contained in:
		
							parent
							
								
									1a2a3b6ac5
								
							
						
					
					
						commit
						98f6b414de
					
				| @ -3,6 +3,7 @@ package main | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"git.wamblee.org/converge/pkg/models" | ||||
| 	"git.wamblee.org/converge/pkg/server/matchmaker" | ||||
| 	"git.wamblee.org/converge/pkg/support/websocketutil" | ||||
| 	"log" | ||||
| @ -15,13 +16,13 @@ import ( | ||||
| 	_ "time/tzdata" | ||||
| ) | ||||
| 
 | ||||
| func parsePublicId(path string) (publicId string, _ error) { | ||||
| func parsePublicId(path string) (publicId models.RendezVousId, _ error) { | ||||
| 	pattern := regexp.MustCompile("/([^/]+)$") | ||||
| 	matches := pattern.FindStringSubmatch(path) | ||||
| 	if len(matches) != 2 { | ||||
| 		return "", fmt.Errorf("Invalid URL path '%s'", path) | ||||
| 	} | ||||
| 	return matches[1], nil | ||||
| 	return models.RendezVousId(matches[1]), nil | ||||
| } | ||||
| 
 | ||||
| func catchAllHandler(contextPath string) func(w http.ResponseWriter, r *http.Request) { | ||||
|  | ||||
| @ -14,14 +14,14 @@ const NAMESPACE = "converge" | ||||
| 
 | ||||
| // more efficient state representation for state
 | ||||
| type PrometheusState struct { | ||||
| 	agents  map[string]*models.Agent | ||||
| 	clients map[string]*models.Client | ||||
| 	agents  map[models.AgentGuid]*models.Agent | ||||
| 	clients map[models.ClientGuid]*models.Client | ||||
| } | ||||
| 
 | ||||
| func NewPrometheusState(state *models.State) *PrometheusState { | ||||
| 	res := PrometheusState{ | ||||
| 		agents:  make(map[string]*models.Agent), | ||||
| 		clients: make(map[string]*models.Client), | ||||
| 		agents:  make(map[models.AgentGuid]*models.Agent), | ||||
| 		clients: make(map[models.ClientGuid]*models.Client), | ||||
| 	} | ||||
| 	for i, _ := range state.Agents { | ||||
| 		res.agents[state.Agents[i].Guid] = &state.Agents[i] | ||||
| @ -121,9 +121,9 @@ var ( | ||||
| 
 | ||||
| func agentLabels(agent *models.Agent) prometheus.Labels { | ||||
| 	return prometheus.Labels{ | ||||
| 		"agent_guid":     agent.Guid, | ||||
| 		"agent_address":  agent.RemoteAddr, | ||||
| 		"agent_id":       agent.PublicId, | ||||
| 		"agent_guid":     string(agent.Guid), | ||||
| 		"agent_address":  string(agent.RemoteAddr), | ||||
| 		"agent_id":       string(agent.PublicId), | ||||
| 		"agent_username": agent.EnvironmentInfo.Username, | ||||
| 		"agent_hostname": agent.EnvironmentInfo.Hostname, | ||||
| 		"agent_pwd":      agent.EnvironmentInfo.Pwd, | ||||
| @ -134,12 +134,12 @@ func agentLabels(agent *models.Agent) prometheus.Labels { | ||||
| 
 | ||||
| func clientLabels(client *models.Client) prometheus.Labels { | ||||
| 	return prometheus.Labels{ | ||||
| 		"client_guid":        client.Guid, | ||||
| 		"client_address":     client.RemoteAddr, | ||||
| 		"client_id":          client.ClientId, | ||||
| 		"agent_id":           client.PublicId, | ||||
| 		"agent_guid":         client.AgentGuid, | ||||
| 		"client_sessiontype": client.SessionType, | ||||
| 		"client_guid":        string(client.Guid), | ||||
| 		"client_address":     string(client.RemoteAddr), | ||||
| 		"client_id":          string(client.ClientId), | ||||
| 		"agent_id":           string(client.PublicId), | ||||
| 		"agent_guid":         string(client.AgentGuid), | ||||
| 		"client_sessiontype": string(client.SessionType), | ||||
| 		"client_username":    client.EnvironmentInfo.Username, | ||||
| 		"client_hostname":    client.EnvironmentInfo.Hostname, | ||||
| 		"client_pwd":         client.EnvironmentInfo.Pwd, | ||||
| @ -154,11 +154,12 @@ func agentActive(agent *models.Agent) { | ||||
| 		removeAgentInfoMetrics(prevAgent) | ||||
| 	} | ||||
| 	agentInfo.With(agentLabels(agent)).Set(1) | ||||
| 	agentGuid := string(agent.Guid) | ||||
| 	agentStartTime. | ||||
| 		With(prometheus.Labels{"agent_guid": agent.Guid}). | ||||
| 		With(prometheus.Labels{"agent_guid": agentGuid}). | ||||
| 		Set(float64(agent.StartTime.UnixMilli())) | ||||
| 	agentDuration. | ||||
| 		With(prometheus.Labels{"agent_guid": agent.Guid}). | ||||
| 		With(prometheus.Labels{"agent_guid": agentGuid}). | ||||
| 		Set(float64(time.Now().Sub(agent.StartTime).Seconds())) | ||||
| } | ||||
| 
 | ||||
| @ -168,12 +169,12 @@ func clientActive(client *models.Client) { | ||||
| 		removeClientInfoMetrics(prevClient) | ||||
| 	} | ||||
| 	clientInfo.With(clientLabels(client)).Set(1) | ||||
| 
 | ||||
| 	clientGuid := string(client.Guid) | ||||
| 	clientStartTime. | ||||
| 		With(prometheus.Labels{"client_guid": client.Guid}). | ||||
| 		With(prometheus.Labels{"client_guid": clientGuid}). | ||||
| 		Set(float64(client.StartTime.UnixMilli())) | ||||
| 	clientDuration. | ||||
| 		With(prometheus.Labels{"client_guid": client.Guid}). | ||||
| 		With(prometheus.Labels{"client_guid": clientGuid}). | ||||
| 		Set(float64(time.Now().Sub(client.StartTime).Seconds())) | ||||
| } | ||||
| 
 | ||||
| @ -223,23 +224,23 @@ func updateMetrics(state *models.State) { | ||||
| func updateDurations() { | ||||
| 	for _, agent := range lastState.agents { | ||||
| 		agentDuration. | ||||
| 			With(prometheus.Labels{"agent_guid": agent.Guid}). | ||||
| 			With(prometheus.Labels{"agent_guid": string(agent.Guid)}). | ||||
| 			Set(float64(time.Now().Sub(agent.StartTime).Seconds())) | ||||
| 	} | ||||
| 	for _, client := range lastState.clients { | ||||
| 		clientDuration. | ||||
| 			With(prometheus.Labels{"client_guid": client.Guid}). | ||||
| 			With(prometheus.Labels{"client_guid": string(client.Guid)}). | ||||
| 			Set(float64(time.Now().Sub(client.StartTime).Seconds())) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func updateMetricsImpl(state *PrometheusState) { | ||||
| 
 | ||||
| 	agentGuids := make(map[string]*models.Agent) | ||||
| 	clientGuids := make(map[string]*models.Client) | ||||
| 	agentGuids := make(map[models.AgentGuid]*models.Agent) | ||||
| 	clientGuids := make(map[models.ClientGuid]*models.Client) | ||||
| 
 | ||||
| 	agentCount.Set(float64(len(state.agents))) | ||||
| 	disconnectedAgents := make(map[string]*models.Agent) | ||||
| 	disconnectedAgents := make(map[models.AgentGuid]*models.Agent) | ||||
| 	for _, agent := range lastState.agents { | ||||
| 		disconnectedAgents[agent.Guid] = agent | ||||
| 	} | ||||
| @ -258,7 +259,7 @@ func updateMetricsImpl(state *PrometheusState) { | ||||
| 	clientCount.Set(float64(len(state.clients))) | ||||
| 
 | ||||
| 	// with this app
 | ||||
| 	disconnectedClients := make(map[string]*models.Client) | ||||
| 	disconnectedClients := make(map[models.ClientGuid]*models.Client) | ||||
| 	for _, client := range lastState.clients { | ||||
| 		disconnectedClients[client.Guid] = client | ||||
| 	} | ||||
| @ -283,7 +284,7 @@ func removeAgentInfoMetrics(agent *models.Agent) bool { | ||||
| 
 | ||||
| func removeAgentMetrics(agent *models.Agent) { | ||||
| 	ok1 := removeAgentInfoMetrics(agent) | ||||
| 	guidLabels := prometheus.Labels{"agent_guid": agent.Guid} | ||||
| 	guidLabels := prometheus.Labels{"agent_guid": string(agent.Guid)} | ||||
| 	ok2 := agentStartTime.Delete(guidLabels) | ||||
| 	// delayed deletion of the duration sow we are sure the prometheus has the last data.
 | ||||
| 	go func() { | ||||
| @ -305,7 +306,7 @@ func removeClientInfoMetrics(client *models.Client) bool { | ||||
| 
 | ||||
| func removeClientMetrics(client *models.Client) { | ||||
| 	ok1 := removeClientInfoMetrics(client) | ||||
| 	guidLabels := prometheus.Labels{"client_guid": client.Guid} | ||||
| 	guidLabels := prometheus.Labels{"client_guid": string(client.Guid)} | ||||
| 	ok2 := clientStartTime.Delete(guidLabels) | ||||
| 	// delayed deletion of the duration sow we are sure the prometheus has the last data.
 | ||||
| 	go func() { | ||||
|  | ||||
| @ -81,7 +81,7 @@ func main() { | ||||
| 
 | ||||
| 		state := models.State{} | ||||
| 		agent := models.Agent{ | ||||
| 			Guid:       strconv.Itoa(rand.Int()), | ||||
| 			Guid:       models.AgentGuid(strconv.Itoa(rand.Int())), | ||||
| 			RemoteAddr: "10.220.1.3:3333", | ||||
| 			PublicId:   "id", | ||||
| 			StartTime:  time.Now().In(japan), | ||||
| @ -96,13 +96,13 @@ func main() { | ||||
| 		} | ||||
| 		state.Agents = append(state.Agents, agent) | ||||
| 		client := models.Client{ | ||||
| 			Guid:        strconv.Itoa(rand.Int()), | ||||
| 			RemoteAddr:  "10.1.3.3", | ||||
| 			PublicId:    "c1", | ||||
| 			AgentGuid:   "12342342", | ||||
| 			ClientId:    "3", | ||||
| 			Guid:        models.ClientGuid(strconv.Itoa(rand.Int())), | ||||
| 			RemoteAddr:  models.RemoteAddr("10.1.3.3"), | ||||
| 			PublicId:    models.RendezVousId("c1"), | ||||
| 			AgentGuid:   models.AgentGuid("12342342"), | ||||
| 			ClientId:    models.ClientId("3"), | ||||
| 			StartTime:   time.Now().In(japan), | ||||
| 			SessionType: "sftp", | ||||
| 			SessionType: models.SessionType("sftp"), | ||||
| 		} | ||||
| 		state.Clients = append(state.Clients, client) | ||||
| 		return templates2.SessionsTab(&state, netherlands) | ||||
|  | ||||
| @ -1,18 +0,0 @@ | ||||
| package models | ||||
| 
 | ||||
| import ( | ||||
| 	"git.wamblee.org/converge/pkg/comms" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type Agent struct { | ||||
| 	Guid       string | ||||
| 	RemoteAddr string | ||||
| 	PublicId   string | ||||
| 	StartTime  time.Time | ||||
| 
 | ||||
| 	// TODO add remote address.
 | ||||
| 
 | ||||
| 	EnvironmentInfo comms.EnvironmentInfo | ||||
| 	ExpiryTime      time.Time | ||||
| } | ||||
| @ -1,17 +1 @@ | ||||
| package models | ||||
| 
 | ||||
| import ( | ||||
| 	"git.wamblee.org/converge/pkg/comms" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type Client struct { | ||||
| 	Guid            string | ||||
| 	RemoteAddr      string | ||||
| 	PublicId        string | ||||
| 	ClientId        string | ||||
| 	AgentGuid       string | ||||
| 	StartTime       time.Time | ||||
| 	SessionType     string | ||||
| 	EnvironmentInfo comms.EnvironmentInfo | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,40 @@ | ||||
| package models | ||||
| 
 | ||||
| import ( | ||||
| 	"git.wamblee.org/converge/pkg/comms" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type RendezVousId string | ||||
| type AgentGuid string | ||||
| type ClientGuid string | ||||
| type ClientId string | ||||
| type SessionType string | ||||
| type RemoteAddr string | ||||
| 
 | ||||
| type Agent struct { | ||||
| 	Guid       AgentGuid | ||||
| 	RemoteAddr RemoteAddr | ||||
| 	PublicId   RendezVousId | ||||
| 	StartTime  time.Time | ||||
| 
 | ||||
| 	// TODO add remote address.
 | ||||
| 
 | ||||
| 	EnvironmentInfo comms.EnvironmentInfo | ||||
| 	ExpiryTime      time.Time | ||||
| } | ||||
| 
 | ||||
| type Client struct { | ||||
| 	Guid            ClientGuid | ||||
| 	RemoteAddr      RemoteAddr | ||||
| 	PublicId        RendezVousId | ||||
| 	ClientId        ClientId | ||||
| 	AgentGuid       AgentGuid | ||||
| 	StartTime       time.Time | ||||
| 	SessionType     SessionType | ||||
| 	EnvironmentInfo comms.EnvironmentInfo | ||||
| } | ||||
| 
 | ||||
| // State is a description of the current state of converge.
 | ||||
| // Created by the server and used for updating the web client
 | ||||
| // and prometheus metrics.
 | ||||
|  | ||||
| @ -30,11 +30,11 @@ type ClientConnection struct { | ||||
| 	clientConnection iowrappers2.ReadWriteAddrCloser | ||||
| } | ||||
| 
 | ||||
| func newAgent(commChannel comms.CommChannel, publicId string, agentInfo comms.EnvironmentInfo) *agentConnection { | ||||
| func newAgent(commChannel comms.CommChannel, publicId models.RendezVousId, agentInfo comms.EnvironmentInfo) *agentConnection { | ||||
| 	return &agentConnection{ | ||||
| 		Agent: models.Agent{ | ||||
| 			Guid:            strconv.Itoa(rand.Int()), | ||||
| 			RemoteAddr:      commChannel.Session.RemoteAddr().String(), | ||||
| 			Guid:            models.AgentGuid(strconv.Itoa(rand.Int())), | ||||
| 			RemoteAddr:      models.RemoteAddr(commChannel.Session.RemoteAddr().String()), | ||||
| 			PublicId:        publicId, | ||||
| 			StartTime:       time.Now(), | ||||
| 			EnvironmentInfo: agentInfo, | ||||
| @ -43,15 +43,15 @@ func newAgent(commChannel comms.CommChannel, publicId string, agentInfo comms.En | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func newClient(publicId string, clientConn iowrappers2.ReadWriteAddrCloser, | ||||
| 	agentConn net.Conn, agentGuid string) *ClientConnection { | ||||
| func newClient(publicId models.RendezVousId, clientConn iowrappers2.ReadWriteAddrCloser, | ||||
| 	agentConn net.Conn, agentGuid models.AgentGuid) *ClientConnection { | ||||
| 	return &ClientConnection{ | ||||
| 		Client: models.Client{ | ||||
| 			Guid:       strconv.Itoa(rand.Int()), | ||||
| 			RemoteAddr: clientConn.RemoteAddr().String(), | ||||
| 			Guid:       models.ClientGuid(strconv.Itoa(rand.Int())), | ||||
| 			RemoteAddr: models.RemoteAddr(clientConn.RemoteAddr().String()), | ||||
| 			PublicId:   publicId, | ||||
| 			AgentGuid:  agentGuid, | ||||
| 			ClientId:   strconv.Itoa(clientIdGenerator.IncrementAndGet()), | ||||
| 			ClientId:   models.ClientId(strconv.Itoa(clientIdGenerator.IncrementAndGet())), | ||||
| 			StartTime:  time.Now(), | ||||
| 		}, | ||||
| 		agentConnection:  agentConn, | ||||
| @ -66,14 +66,14 @@ func (match *ClientConnection) Synchronize() { | ||||
| type Admin struct { | ||||
| 	// map of public id to agent
 | ||||
| 	mutex   sync.Mutex | ||||
| 	agents  map[string]*agentConnection | ||||
| 	agents  map[models.RendezVousId]*agentConnection | ||||
| 	clients []*ClientConnection | ||||
| } | ||||
| 
 | ||||
| func NewAdmin() *Admin { | ||||
| 	return &Admin{ | ||||
| 		mutex:   sync.Mutex{}, | ||||
| 		agents:  make(map[string]*agentConnection), | ||||
| 		agents:  make(map[models.RendezVousId]*agentConnection), | ||||
| 		clients: make([]*ClientConnection, 0), // not strictly needed
 | ||||
| 	} | ||||
| } | ||||
| @ -93,8 +93,8 @@ func (admin *Admin) CreateNotifification() *models.State { | ||||
| 	return &state | ||||
| } | ||||
| 
 | ||||
| func (admin *Admin) getFreeId(publicId string) (string, error) { | ||||
| 	usedIds := make(map[string]bool) | ||||
| func (admin *Admin) getFreeId(publicId models.RendezVousId) (models.RendezVousId, error) { | ||||
| 	usedIds := make(map[models.RendezVousId]bool) | ||||
| 	for _, agent := range admin.agents { | ||||
| 		usedIds[agent.PublicId] = true | ||||
| 	} | ||||
| @ -103,16 +103,16 @@ func (admin *Admin) getFreeId(publicId string) (string, error) { | ||||
| 	} | ||||
| 	if usedIds[publicId] { | ||||
| 		for i := 0; i < 100; i++ { | ||||
| 			candidate := publicId + "-" + strconv.Itoa(i) | ||||
| 			if !usedIds[candidate] { | ||||
| 				return candidate, nil | ||||
| 			candidate := string(publicId) + "-" + strconv.Itoa(i) | ||||
| 			if !usedIds[models.RendezVousId(candidate)] { | ||||
| 				return models.RendezVousId(candidate), nil | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return "", fmt.Errorf("Could not allocate agent id based on requested public id '%s'", publicId) | ||||
| } | ||||
| 
 | ||||
| func (admin *Admin) AddAgent(publicId string, agentInfo comms.EnvironmentInfo, conn io.ReadWriteCloser) (*agentConnection, error) { | ||||
| func (admin *Admin) AddAgent(publicId models.RendezVousId, agentInfo comms.EnvironmentInfo, conn io.ReadWriteCloser) (*agentConnection, error) { | ||||
| 	admin.mutex.Lock() | ||||
| 	defer admin.mutex.Unlock() | ||||
| 
 | ||||
| @ -126,7 +126,7 @@ func (admin *Admin) AddAgent(publicId string, agentInfo comms.EnvironmentInfo, c | ||||
| 		comms.SendRegistrationMessage(conn, comms.AgentRegistration{ | ||||
| 			Ok:      true, | ||||
| 			Message: message, | ||||
| 			Id:      publicId, | ||||
| 			Id:      string(publicId), | ||||
| 		}) | ||||
| 	} else { | ||||
| 		comms.SendRegistrationMessage(conn, comms.AgentRegistration{ | ||||
| @ -149,7 +149,7 @@ func (admin *Admin) AddAgent(publicId string, agentInfo comms.EnvironmentInfo, c | ||||
| 	return agent, nil | ||||
| } | ||||
| 
 | ||||
| func (admin *Admin) AddClient(publicId string, clientConn iowrappers2.ReadWriteAddrCloser) (*ClientConnection, error) { | ||||
| func (admin *Admin) AddClient(publicId models.RendezVousId, clientConn iowrappers2.ReadWriteAddrCloser) (*ClientConnection, error) { | ||||
| 	admin.mutex.Lock() | ||||
| 	defer admin.mutex.Unlock() | ||||
| 
 | ||||
| @ -172,7 +172,7 @@ func (admin *Admin) AddClient(publicId string, clientConn iowrappers2.ReadWriteA | ||||
| 	// Before using this connection for SSH we use it to send client metadata to the
 | ||||
| 	// agent
 | ||||
| 	err = comms.SendClientInfo(agentConn, comms.ClientInfo{ | ||||
| 		ClientId: client.ClientId, | ||||
| 		ClientId: string(client.ClientId), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| @ -194,7 +194,7 @@ func (admin *Admin) getAgentConnection(agent *agentConnection) (net.Conn, error) | ||||
| 	return agentConn, err | ||||
| } | ||||
| 
 | ||||
| func (admin *Admin) RemoveAgent(publicId string) error { | ||||
| func (admin *Admin) RemoveAgent(publicId models.RendezVousId) error { | ||||
| 	admin.mutex.Lock() | ||||
| 	defer admin.mutex.Unlock() | ||||
| 
 | ||||
| @ -230,7 +230,7 @@ func (admin *Admin) RemoveClient(client *ClientConnection) error { | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (admin *Admin) SetSessionType(clientId string, sessionType string) { | ||||
| func (admin *Admin) SetSessionType(clientId models.ClientId, sessionType models.SessionType) { | ||||
| 	admin.mutex.Lock() | ||||
| 	defer admin.mutex.Unlock() | ||||
| 	for _, client := range admin.clients { | ||||
|  | ||||
| @ -25,7 +25,7 @@ func NewMatchMaker(notifier Notifier) *MatchMaker { | ||||
| 	return &converge | ||||
| } | ||||
| 
 | ||||
| func (converge *MatchMaker) Register(publicId string, conn io.ReadWriteCloser) error { | ||||
| func (converge *MatchMaker) Register(publicId models.RendezVousId, conn io.ReadWriteCloser) error { | ||||
| 
 | ||||
| 	serverInfo := comms.ServerInfo{} | ||||
| 
 | ||||
| @ -53,7 +53,7 @@ func (converge *MatchMaker) Register(publicId string, conn io.ReadWriteCloser) e | ||||
| 			}, | ||||
| 			func(session comms.SessionInfo) { | ||||
| 				log.Println("Recceived sessioninfo ", session) | ||||
| 				converge.admin.SetSessionType(session.ClientId, session.SessionType) | ||||
| 				converge.admin.SetSessionType(models.ClientId(session.ClientId), models.SessionType(session.SessionType)) | ||||
| 			}, | ||||
| 			func(expiry comms.ExpiryTimeUpdate) { | ||||
| 				agent.ExpiryTime = expiry.ExpiryTime | ||||
| @ -68,7 +68,7 @@ func (converge *MatchMaker) Register(publicId string, conn io.ReadWriteCloser) e | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (converge *MatchMaker) Connect(wsProxyMode bool, publicId string, conn iowrappers2.ReadWriteAddrCloser) error { | ||||
| func (converge *MatchMaker) Connect(wsProxyMode bool, publicId models.RendezVousId, conn iowrappers2.ReadWriteAddrCloser) error { | ||||
| 	defer conn.Close() | ||||
| 
 | ||||
| 	log.Printf("Using wsproxy protocol %v", wsProxyMode) | ||||
|  | ||||
| @ -48,7 +48,7 @@ templ State(state *models.State, location *time.Location) { | ||||
|            </thead> | ||||
|    for _, agent := range state.Agents { | ||||
|            <tr> | ||||
|                <td>{agent.PublicId}</td> | ||||
|                <td>{string(agent.PublicId)}</td> | ||||
|                <td>{agent.StartTime.In(location).Format(time.DateTime)}</td> | ||||
|                <td>{agent.ExpiryTime.In(location).Format(time.DateTime)}</td> | ||||
|                <td>{agent.EnvironmentInfo.Username}</td> | ||||
| @ -82,10 +82,10 @@ templ State(state *models.State, location *time.Location) { | ||||
|               </thead> | ||||
|    for _, client := range state.Clients { | ||||
|               <tr> | ||||
|                   <td>{client.ClientId}</td> | ||||
|                   <td>{string(client.ClientId)}</td> | ||||
|                   <td>{client.StartTime.In(location).Format(time.DateTime)}</td> | ||||
|                   <td>{client.SessionType}</td> | ||||
|                   <td>{client.PublicId}</td> | ||||
|                   <td>{string(client.SessionType)}</td> | ||||
|                   <td>{string(client.PublicId)}</td> | ||||
|                   <td>{client.EnvironmentInfo.Username}</td> | ||||
|                   <td>{client.EnvironmentInfo.Hostname}</td> | ||||
|                   <td>{client.EnvironmentInfo.OS}</td> | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user