accounts and email
This commit is contained in:
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/tim/cairnquire/apps/server/internal/auth"
|
||||
"github.com/tim/cairnquire/apps/server/internal/docs"
|
||||
)
|
||||
|
||||
@@ -29,6 +30,7 @@ type layoutData struct {
|
||||
BodyClass string
|
||||
BodyTemplate string
|
||||
Data any
|
||||
Principal *auth.Principal
|
||||
}
|
||||
|
||||
type indexData struct {
|
||||
@@ -43,6 +45,7 @@ type documentData struct {
|
||||
Hash string
|
||||
HTML template.HTML
|
||||
Breadcrumbs []breadcrumb
|
||||
CanWrite bool
|
||||
}
|
||||
|
||||
type documentEditData struct {
|
||||
@@ -53,6 +56,7 @@ type documentEditData struct {
|
||||
Hash string
|
||||
Source string
|
||||
Breadcrumbs []breadcrumb
|
||||
CanWrite bool
|
||||
}
|
||||
|
||||
type breadcrumb struct {
|
||||
@@ -98,7 +102,7 @@ func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
activeFolder := strings.Trim(r.URL.Query().Get("folder"), "/")
|
||||
s.renderTemplate(w, http.StatusOK, "index.gohtml", layoutData{
|
||||
s.renderTemplate(w, r, http.StatusOK, "index.gohtml", layoutData{
|
||||
Title: "Cairnquire",
|
||||
BodyClass: "page-index",
|
||||
BodyTemplate: "index_content",
|
||||
@@ -117,7 +121,7 @@ func (s *Server) handleSearchPage(w http.ResponseWriter, r *http.Request, query
|
||||
s.logger.Warn("search failed", "error", err)
|
||||
}
|
||||
|
||||
s.renderTemplate(w, http.StatusOK, "search.gohtml", layoutData{
|
||||
s.renderTemplate(w, r, http.StatusOK, "search.gohtml", layoutData{
|
||||
Title: "Search: " + query,
|
||||
BodyClass: "page-search",
|
||||
BodyTemplate: "search_content",
|
||||
@@ -223,7 +227,9 @@ func (s *Server) renderDocumentEditor(w http.ResponseWriter, r *http.Request, pa
|
||||
return
|
||||
}
|
||||
|
||||
s.renderTemplate(w, http.StatusOK, "document_edit.gohtml", layoutData{
|
||||
canWrite := s.canWriteDocuments(r)
|
||||
|
||||
s.renderTemplate(w, r, http.StatusOK, "document_edit.gohtml", layoutData{
|
||||
Title: "Edit: " + page.Title,
|
||||
BodyClass: "page-document-edit",
|
||||
BodyTemplate: "document_edit_content",
|
||||
@@ -235,6 +241,7 @@ func (s *Server) renderDocumentEditor(w http.ResponseWriter, r *http.Request, pa
|
||||
Hash: page.Hash,
|
||||
Source: page.Content,
|
||||
Breadcrumbs: buildBreadcrumbs(page.Path),
|
||||
CanWrite: canWrite,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -256,7 +263,9 @@ func (s *Server) renderDocumentPage(w http.ResponseWriter, r *http.Request, page
|
||||
return
|
||||
}
|
||||
|
||||
s.renderTemplate(w, http.StatusOK, "document.gohtml", layoutData{
|
||||
canWrite := s.canWriteDocuments(r)
|
||||
|
||||
s.renderTemplate(w, r, http.StatusOK, "document.gohtml", layoutData{
|
||||
Title: page.Title,
|
||||
BodyClass: "page-document",
|
||||
BodyTemplate: "document_content",
|
||||
@@ -268,11 +277,20 @@ func (s *Server) renderDocumentPage(w http.ResponseWriter, r *http.Request, page
|
||||
Hash: page.Hash,
|
||||
HTML: template.HTML(page.HTML),
|
||||
Breadcrumbs: buildBreadcrumbs(page.Path),
|
||||
CanWrite: canWrite,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
s.handleReadyz(w, r)
|
||||
}
|
||||
|
||||
func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) handleReadyz(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -286,11 +304,18 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
contentStatus = err.Error()
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
ready := dbStatus == "ok" && contentStatus == "ok"
|
||||
payload := map[string]string{
|
||||
"status": "ok",
|
||||
"database": dbStatus,
|
||||
"contentStore": contentStatus,
|
||||
})
|
||||
}
|
||||
if !ready {
|
||||
payload["status"] = "unavailable"
|
||||
writeJSONWithStatus(w, http.StatusServiceUnavailable, payload)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, payload)
|
||||
}
|
||||
|
||||
func (s *Server) handleServiceWorker(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -515,7 +540,13 @@ func (s *Server) handleAttachment(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(content)
|
||||
}
|
||||
|
||||
func (s *Server) renderTemplate(w http.ResponseWriter, status int, name string, data layoutData) {
|
||||
func (s *Server) renderTemplate(w http.ResponseWriter, r *http.Request, status int, name string, data layoutData) {
|
||||
if data.Principal == nil {
|
||||
if principal, ok := principalFromContext(r.Context()); ok {
|
||||
p := principal
|
||||
data.Principal = &p
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
if err := s.templates.ExecuteTemplate(w, name, data); err != nil {
|
||||
@@ -524,7 +555,7 @@ func (s *Server) renderTemplate(w http.ResponseWriter, status int, name string,
|
||||
}
|
||||
|
||||
func (s *Server) renderError(w http.ResponseWriter, r *http.Request, status int, message string) {
|
||||
s.renderTemplate(w, status, "error.gohtml", layoutData{
|
||||
s.renderTemplate(w, r, status, "error.gohtml", layoutData{
|
||||
Title: http.StatusText(status),
|
||||
BodyClass: "page-error",
|
||||
BodyTemplate: "error_content",
|
||||
@@ -649,8 +680,30 @@ func (s *Server) handleDocuments(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"documents": results})
|
||||
}
|
||||
|
||||
func (s *Server) handleDocumentSave(w http.ResponseWriter, r *http.Request) {
|
||||
pagePath := chi.URLParam(r, "*")
|
||||
func (s *Server) handleDocumentMutation(w http.ResponseWriter, r *http.Request) {
|
||||
pagePath, err := unescapeDocumentRoutePath(chi.URLParam(r, "*"))
|
||||
if err != nil {
|
||||
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid document path"})
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case strings.HasSuffix(pagePath, "/archive"):
|
||||
s.handleDocumentArchivePath(w, r, strings.TrimSuffix(pagePath, "/archive"))
|
||||
case strings.HasSuffix(pagePath, "/restore"):
|
||||
s.handleDocumentRestorePath(w, r, strings.TrimSuffix(pagePath, "/restore"))
|
||||
default:
|
||||
s.handleDocumentSavePath(w, r, pagePath)
|
||||
}
|
||||
}
|
||||
|
||||
func unescapeDocumentRoutePath(path string) (string, error) {
|
||||
if path == "" {
|
||||
return "", nil
|
||||
}
|
||||
return url.PathUnescape(path)
|
||||
}
|
||||
|
||||
func (s *Server) handleDocumentSavePath(w http.ResponseWriter, r *http.Request, pagePath string) {
|
||||
if pagePath == "" {
|
||||
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "document path is required"})
|
||||
return
|
||||
@@ -705,6 +758,50 @@ func writeJSONWithStatus(w http.ResponseWriter, status int, payload any) {
|
||||
_ = json.NewEncoder(w).Encode(payload)
|
||||
}
|
||||
|
||||
func (s *Server) canWriteDocuments(r *http.Request) bool {
|
||||
principal, ok := principalFromContext(r.Context())
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return auth.Allows(principal, auth.ScopeDocsWrite)
|
||||
}
|
||||
|
||||
func (s *Server) handleDocumentArchivePath(w http.ResponseWriter, r *http.Request, pagePath string) {
|
||||
if pagePath == "" {
|
||||
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "document path is required"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.documents.ArchiveDocument(r.Context(), pagePath); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
writeJSONWithStatus(w, http.StatusNotFound, map[string]string{"error": "document not found"})
|
||||
return
|
||||
}
|
||||
writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONWithStatus(w, http.StatusOK, map[string]string{"status": "archived"})
|
||||
}
|
||||
|
||||
func (s *Server) handleDocumentRestorePath(w http.ResponseWriter, r *http.Request, pagePath string) {
|
||||
if pagePath == "" {
|
||||
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "document path is required"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.documents.RestoreDocument(r.Context(), pagePath); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
writeJSONWithStatus(w, http.StatusNotFound, map[string]string{"error": "document not found"})
|
||||
return
|
||||
}
|
||||
writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONWithStatus(w, http.StatusOK, map[string]string{"status": "restored"})
|
||||
}
|
||||
|
||||
func trimEditSuffix(path string) (string, bool) {
|
||||
if path == "edit" {
|
||||
return "", true
|
||||
|
||||
Reference in New Issue
Block a user