Initial import: KeepNote service (dcntrlized-auth consumer)
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
// Copyright (C) 2026 Dcntrlized
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.dcntrlized.org/dcntrlized/dcntrlized-auth/authproxy"
|
||||
)
|
||||
|
||||
// Login proxies the Maestro-mode action to /auth/login. The {payload} envelope
|
||||
// was already unwrapped by the handler; the inner body is forwarded raw and
|
||||
// the auth response is relayed unwrapped.
|
||||
func Login(w http.ResponseWriter, r *http.Request, payload interface{}) {
|
||||
Debug("login", "POST", "proxying to auth service")
|
||||
authproxy.ProxyPayload(w, r, "login", payload)
|
||||
}
|
||||
|
||||
// Register proxies the Maestro-mode action to /auth/register.
|
||||
func Register(w http.ResponseWriter, r *http.Request, payload interface{}) {
|
||||
Debug("register", "POST", "proxying to auth service")
|
||||
authproxy.ProxyPayload(w, r, "register", payload)
|
||||
}
|
||||
|
||||
// Logout proxies the GET action variant to /auth/logout (forwarded bodyless;
|
||||
// the auth service keeps GET a no-op).
|
||||
func Logout(w http.ResponseWriter, r *http.Request, payload interface{}) {
|
||||
Debug("logout", "GET", "proxying to auth service")
|
||||
authproxy.ProxyEmpty(w, r, "logout")
|
||||
}
|
||||
|
||||
// Refresh proxies the Maestro-mode refresh action (M5 auto-refresh path).
|
||||
func Refresh(w http.ResponseWriter, r *http.Request, payload interface{}) {
|
||||
Debug("refresh", "POST", "proxying to auth service")
|
||||
authproxy.ProxyPayload(w, r, "refresh", payload)
|
||||
}
|
||||
|
||||
// Delete proxies the Maestro-mode delete action to /auth/delete. The payload
|
||||
// carries the password only; the X-Access-Token header is forwarded unchanged.
|
||||
func Delete(w http.ResponseWriter, r *http.Request, payload interface{}) {
|
||||
Debug("delete", "POST", "proxying to auth service")
|
||||
authproxy.ProxyPayload(w, r, "delete", payload)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"git.dcntrlized.org/Dcntrlized/KeepNote/db"
|
||||
"git.dcntrlized.org/dcntrlized/dcntrlized-auth/authclient"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type deleteNoteRequest struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
}
|
||||
|
||||
func DeleteNote(w http.ResponseWriter, r *http.Request, payload interface{}) error {
|
||||
Debug("deleteNote", "DELETE", "called")
|
||||
principal, err := authclient.Authorize(r)
|
||||
if err != nil {
|
||||
Warn("deleteNote", "DELETE", "authorization failed: %v", err)
|
||||
unauthorized(w, err)
|
||||
return err
|
||||
}
|
||||
|
||||
var req deleteNoteRequest
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
Warn("deleteNote", "DELETE", "invalid payload: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(payloadBytes, &req); err != nil {
|
||||
Warn("deleteNote", "DELETE", "invalid payload: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return err
|
||||
}
|
||||
if req.ID == uuid.Nil {
|
||||
Warn("deleteNote", "DELETE", "missing note id")
|
||||
http.Error(w, "missing note id", http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
Debug("deleteNote", "DELETE", "deleting note %s for user %s", req.ID, principal.ID)
|
||||
if err := db.DeleteNote(req.ID, principal.ID); err != nil {
|
||||
Error("deleteNote", "DELETE", "failed to delete note: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
|
||||
Debug("deleteNote", "DELETE", "note %s deleted", req.ID)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (C) 2026 Dcntrlized
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"git.dcntrlized.org/dcntrlized/dcntrlized-auth/authclient"
|
||||
)
|
||||
|
||||
// unauthorized writes the structured 401 body mapping authclient errors to
|
||||
// the M1 vocabulary (ERR_AUTH_FAILED / ERR_TOKEN_EXPIRED / ERR_TOKEN_INVALID).
|
||||
func unauthorized(w http.ResponseWriter, err error) {
|
||||
code, message := "ERR_AUTH_FAILED", "unauthorized"
|
||||
switch {
|
||||
case errors.Is(err, authclient.ErrTokenExpired):
|
||||
code, message = "ERR_TOKEN_EXPIRED", "access token expired"
|
||||
case errors.Is(err, authclient.ErrTokenInvalid):
|
||||
code, message = "ERR_TOKEN_INVALID", "invalid access token"
|
||||
}
|
||||
writeStructuredError(w, http.StatusUnauthorized, code, message, "")
|
||||
}
|
||||
|
||||
// writeStructuredError emits {"error":{"code","message","details"}} — the same
|
||||
// body shape the auth service returns (export theme M2.3).
|
||||
func writeStructuredError(w http.ResponseWriter, status int, code, message, details string) {
|
||||
body := struct {
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details string `json:"details,omitempty"`
|
||||
} `json:"error"`
|
||||
}{}
|
||||
body.Error.Code = code
|
||||
body.Error.Message = message
|
||||
body.Error.Details = details
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"git.dcntrlized.org/Dcntrlized/KeepNote/db"
|
||||
"git.dcntrlized.org/dcntrlized/dcntrlized-auth/authclient"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func GetNotes(w http.ResponseWriter, r *http.Request) {
|
||||
Debug("getNotes", "GET", "called")
|
||||
principal, err := authclient.Authorize(r)
|
||||
if err != nil {
|
||||
Warn("getNotes", "GET", "authorization failed: %v", err)
|
||||
unauthorized(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Proceed to fetch and return notes for the authenticated user.
|
||||
Debug("getNotes", "GET", "fetching notes for user %s", principal.ID)
|
||||
notes, err := db.QueryNotes(principal.ID)
|
||||
if err != nil {
|
||||
Error("getNotes", "GET", "failed to query notes: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(notes)
|
||||
}
|
||||
|
||||
func GetNote(w http.ResponseWriter, r *http.Request) {
|
||||
Debug("getNote", "GET", "called")
|
||||
principal, err := authclient.Authorize(r)
|
||||
if err != nil {
|
||||
Warn("getNote", "GET", "authorization failed: %v", err)
|
||||
unauthorized(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(r.URL.Query().Get("id"))
|
||||
if err != nil {
|
||||
Warn("getNote", "GET", "missing or invalid note id: %v", err)
|
||||
http.Error(w, "missing or invalid note id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
Debug("getNote", "GET", "fetching note %s for user %s", id, principal.ID)
|
||||
note, err := db.GetNote(id, principal.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
Warn("getNote", "GET", "note %s not found for user %s", id, principal.ID)
|
||||
http.Error(w, "note not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
Error("getNote", "GET", "failed to fetch note: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(note)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.dcntrlized.org/Dcntrlized/KeepNote/logger"
|
||||
)
|
||||
|
||||
func init() {
|
||||
for _, name := range []string{
|
||||
"git.dcntrlized.org/Dcntrlized/KeepNote/actions.Debug",
|
||||
"git.dcntrlized.org/Dcntrlized/KeepNote/actions.Warn",
|
||||
"git.dcntrlized.org/Dcntrlized/KeepNote/actions.Error",
|
||||
"git.dcntrlized.org/Dcntrlized/KeepNote/actions.logAction",
|
||||
} {
|
||||
logger.RegisterLogHelper(name)
|
||||
}
|
||||
}
|
||||
|
||||
func Debug(action, verb, format string, v ...any) {
|
||||
logAction(logger.Debug, action, verb, format, v...)
|
||||
}
|
||||
|
||||
func Warn(action, verb, format string, v ...any) {
|
||||
logAction(logger.Warn, action, verb, format, v...)
|
||||
}
|
||||
|
||||
func Error(action, verb, format string, v ...any) {
|
||||
logAction(logger.Error, action, verb, format, v...)
|
||||
}
|
||||
|
||||
func logAction(logf func(format string, v ...any), action, verb, format string, v ...any) {
|
||||
logf("[action=%s verb=%s] %s", action, verb, fmt.Sprintf(format, v...))
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
========== Start Log ==========
|
||||
[INFO] Logger initialized
|
||||
[2026-08-09T17:55:57+01:00]
|
||||
@@ -0,0 +1,4 @@
|
||||
========== Start Log ==========
|
||||
[INFO] Logger initialized
|
||||
[2026-08-09T17:55:57+01:00]
|
||||
ERROR: 2026/08/09 16:55:57 actions_test.go:6: [action=getNotes verb=GET] boom
|
||||
@@ -0,0 +1,3 @@
|
||||
========== Start Log ==========
|
||||
[INFO] Logger initialized
|
||||
[2026-08-09T17:55:57+01:00]
|
||||
@@ -0,0 +1,5 @@
|
||||
========== Start Log ==========
|
||||
[INFO] Logger initialized
|
||||
[2026-08-09T17:55:57+01:00]
|
||||
WARN: 2026/08/09 16:55:57 JWT_ACCESS_SECRET is not set
|
||||
WARN: 2026/08/09 16:55:57 JWT_REFRESH_SECRET is not set
|
||||
@@ -0,0 +1,52 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"git.dcntrlized.org/Dcntrlized/KeepNote/db"
|
||||
"git.dcntrlized.org/dcntrlized/dcntrlized-auth/authclient"
|
||||
)
|
||||
|
||||
func WriteNote(w http.ResponseWriter, r *http.Request, payload interface{}) (*db.Note, error) {
|
||||
Debug("writeNote", "POST", "called with payload %v", payload)
|
||||
principal, err := authclient.Authorize(r)
|
||||
if err != nil {
|
||||
Warn("writeNote", "POST", "authorization failed: %v", err)
|
||||
unauthorized(w, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
note, err := parseNoteFromPayload(payload)
|
||||
if err != nil {
|
||||
Warn("writeNote", "POST", "invalid payload: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Debug("writeNote", "POST", "persisting note for user %s", principal.ID)
|
||||
note, err = db.WriteNote(¬e, principal.ID)
|
||||
if err != nil {
|
||||
Error("writeNote", "POST", "failed to persist note: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Debug("writeNote", "POST", "note %s created", note.ID)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(note)
|
||||
|
||||
return ¬e, nil
|
||||
}
|
||||
|
||||
func parseNoteFromPayload(payload interface{}) (db.Note, error) {
|
||||
var note db.Note
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return db.Note{}, err
|
||||
}
|
||||
if err := json.Unmarshal(payloadBytes, ¬e); err != nil {
|
||||
return db.Note{}, err
|
||||
}
|
||||
return note, nil
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"git.dcntrlized.org/Dcntrlized/KeepNote/db"
|
||||
"git.dcntrlized.org/dcntrlized/dcntrlized-auth/authclient"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type updateNoteRequest struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
func UpdateNote(w http.ResponseWriter, r *http.Request, payload interface{}) error {
|
||||
Debug("updateNote", "PUT", "called")
|
||||
principal, err := authclient.Authorize(r)
|
||||
if err != nil {
|
||||
Warn("updateNote", "PUT", "authorization failed: %v", err)
|
||||
unauthorized(w, err)
|
||||
return err
|
||||
}
|
||||
|
||||
var req updateNoteRequest
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
Warn("updateNote", "PUT", "invalid payload: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(payloadBytes, &req); err != nil {
|
||||
Warn("updateNote", "PUT", "invalid payload: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return err
|
||||
}
|
||||
if req.ID == uuid.Nil {
|
||||
Warn("updateNote", "PUT", "missing note id")
|
||||
http.Error(w, "missing note id", http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
Debug("updateNote", "PUT", "updating note %s for user %s", req.ID, principal.ID)
|
||||
if _, err := db.UpdateNote(req.ID, principal.ID, req.Title, req.Content); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
Warn("updateNote", "PUT", "note %s not found for user %s", req.ID, principal.ID)
|
||||
http.Error(w, "note not found", http.StatusNotFound)
|
||||
return nil
|
||||
}
|
||||
Error("updateNote", "PUT", "failed to update note: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.ReplaceNoteTags(req.ID, req.Tags); err != nil {
|
||||
Error("updateNote", "PUT", "failed to set tags: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
|
||||
note, err := db.GetNote(req.ID, principal.ID)
|
||||
if err != nil {
|
||||
Error("updateNote", "PUT", "failed to reload note: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
|
||||
Debug("updateNote", "PUT", "note %s updated", req.ID)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(note)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user