Add workspace management foundation
Test and publish / verify (push) Has been cancelled

This commit is contained in:
2026-08-02 23:17:42 +03:30
parent b0f486f026
commit 06fa677ec6
5 changed files with 121 additions and 1 deletions
+33
View File
@@ -89,6 +89,8 @@ type PageData struct {
ReportEnd string
SelectedSection string
Users []User
Workspaces []Workspace
Workspace Workspace
Day DayDetail
Week WeekDetail
WorkUpdates []WorkUpdate
@@ -303,6 +305,9 @@ func (s *Server) routes(mux *http.ServeMux) {
mux.HandleFunc("GET /admin/users", s.requireAdmin(s.usersPage))
mux.HandleFunc("POST /admin/users", s.requireAdmin(s.csrf(s.createUser)))
mux.HandleFunc("POST /admin/users/{id}/status", s.requireAdmin(s.csrf(s.setUserStatus)))
mux.HandleFunc("GET /admin/workspaces", s.requireAdmin(s.workspacesPage))
mux.HandleFunc("POST /admin/workspaces", s.requireAdmin(s.csrf(s.createWorkspace)))
mux.HandleFunc("POST /workspace/switch", s.requireAuth(s.csrf(s.switchWorkspace)))
mux.HandleFunc("GET /reports", s.requireAdmin(s.reportPage))
mux.HandleFunc("GET /reports/attendance.csv", s.requireAdmin(s.reportCSV))
mux.HandleFunc("GET /reports/work-updates.csv", s.requireAdmin(s.workUpdatesCSV))
@@ -481,6 +486,12 @@ func (s *Server) health(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) render(w http.ResponseWriter, name string, data PageData) {
if data.User != nil {
if workspaces, err := s.store.Workspaces(data.User.ID); err == nil {
data.Workspaces = workspaces
if data.Workspace.ID == 0 && len(workspaces) > 0 { data.Workspace = workspaces[0] }
}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := s.templates.ExecuteTemplate(w, name, data); err != nil {
log.Printf("render %s: %v", name, err)
@@ -1053,6 +1064,28 @@ func (s *Server) usersPage(w http.ResponseWriter, r *http.Request) {
})
}
func (s *Server) workspacesPage(w http.ResponseWriter, r *http.Request) {
u := currentUser(r)
workspaces, err := s.store.Workspaces(u.ID)
if err != nil { http.Error(w, "could not load workspaces", http.StatusInternalServerError); return }
s.render(w, "workspaces.html", PageData{Title:"Workspaces", User:u, CSRF:csrfToken(r), Workspaces:workspaces, Error:r.URL.Query().Get("error"), Flash:r.URL.Query().Get("flash"), SelectedSection:"workspaces"})
}
func (s *Server) createWorkspace(w http.ResponseWriter, r *http.Request) {
u := currentUser(r)
if _, err := s.store.CreateWorkspace(r.FormValue("name"), r.FormValue("slug"), u.ID); err != nil { http.Redirect(w,r,"/admin/workspaces?error="+url.QueryEscape(err.Error()),http.StatusSeeOther); return }
http.Redirect(w,r,"/admin/workspaces?flash=Workspace+created",http.StatusSeeOther)
}
func (s *Server) switchWorkspace(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.FormValue("workspace_id"),10,64)
if err != nil { http.Error(w,"invalid workspace",http.StatusBadRequest); return }
if _, err := s.store.WorkspaceMember(id,currentUser(r).ID); err != nil { http.Error(w,"workspace access denied",http.StatusForbidden); return }
http.SetCookie(w,&http.Cookie{Name:"teammate_workspace",Value:strconv.FormatInt(id,10),Path:"/",HttpOnly:true,SameSite:http.SameSiteLaxMode,MaxAge:31536000})
returnTo := r.FormValue("return_to"); if returnTo == "" || !strings.HasPrefix(returnTo,"/") { returnTo = "/" }
http.Redirect(w,r,returnTo,http.StatusSeeOther)
}
func (s *Server) createUser(w http.ResponseWriter, r *http.Request) {
_, err := s.store.CreateUser(r.FormValue("username"), r.FormValue("password"), r.FormValue("display_name"), r.FormValue("email"), r.FormValue("role"))
if err != nil {