phase 2 initial test
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func jsonError(w http.ResponseWriter, message string, status int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": message})
|
||||
}
|
||||
|
||||
func jsonOK(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
@@ -1,22 +1,53 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/forgeo/forgebucket/internal/models"
|
||||
)
|
||||
|
||||
type PipelineHandler struct{}
|
||||
type PipelineHandler struct {
|
||||
db *xorm.Engine
|
||||
}
|
||||
|
||||
func NewPipelineHandler() *PipelineHandler {
|
||||
return &PipelineHandler{}
|
||||
func NewPipelineHandler(db *xorm.Engine) *PipelineHandler {
|
||||
return &PipelineHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *PipelineHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode([]any{})
|
||||
ownerName := chi.URLParam(r, "owner")
|
||||
repoName := chi.URLParam(r, "repo")
|
||||
|
||||
repoID, ok := h.repoID(w, ownerName, repoName)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = repoID
|
||||
// Pipeline records will be added in Phase 3 (CI integration).
|
||||
// Return empty list so the client doesn't break.
|
||||
jsonOK(w, []any{})
|
||||
}
|
||||
|
||||
func (h *PipelineHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
jsonError(w, "not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (h *PipelineHandler) repoID(w http.ResponseWriter, ownerName, repoName string) (int64, bool) {
|
||||
var owner models.User
|
||||
found, err := h.db.Where("username = ?", ownerName).Get(&owner)
|
||||
if err != nil || !found {
|
||||
jsonError(w, "repository not found", http.StatusNotFound)
|
||||
return 0, false
|
||||
}
|
||||
|
||||
var repo models.Repository
|
||||
found, err = h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo)
|
||||
if err != nil || !found {
|
||||
jsonError(w, "repository not found", http.StatusNotFound)
|
||||
return 0, false
|
||||
}
|
||||
return repo.ID, true
|
||||
}
|
||||
|
||||
+152
-10
@@ -3,31 +3,173 @@ package handlers
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/forgeo/forgebucket/internal/api/middleware"
|
||||
"github.com/forgeo/forgebucket/internal/models"
|
||||
)
|
||||
|
||||
type PRHandler struct{}
|
||||
type PRHandler struct {
|
||||
db *xorm.Engine
|
||||
}
|
||||
|
||||
func NewPRHandler() *PRHandler {
|
||||
return &PRHandler{}
|
||||
func NewPRHandler(db *xorm.Engine) *PRHandler {
|
||||
return &PRHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *PRHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode([]any{})
|
||||
repoID, ok := h.repoIDFromURL(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
status := r.URL.Query().Get("status")
|
||||
|
||||
sess := h.db.Where("repo_id = ?", repoID)
|
||||
if status != "" {
|
||||
sess = sess.And("status = ?", status)
|
||||
}
|
||||
|
||||
var prs []models.PullRequest
|
||||
if err := sess.Find(&prs); err != nil {
|
||||
jsonError(w, "could not list pull requests", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if prs == nil {
|
||||
prs = []models.PullRequest{}
|
||||
}
|
||||
jsonOK(w, prs)
|
||||
}
|
||||
|
||||
func (h *PRHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
repoID, ok := h.repoIDFromURL(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
authorID, _ := middleware.UserIDFromContext(r.Context())
|
||||
|
||||
var body struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
SourceBranch string `json:"sourceBranch"`
|
||||
TargetBranch string `json:"targetBranch"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
jsonError(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.Title == "" || body.SourceBranch == "" {
|
||||
jsonError(w, "title and sourceBranch are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.TargetBranch == "" {
|
||||
body.TargetBranch = "main"
|
||||
}
|
||||
|
||||
pr := &models.PullRequest{
|
||||
RepoID: repoID,
|
||||
AuthorID: authorID,
|
||||
Title: body.Title,
|
||||
Body: body.Body,
|
||||
SourceBranch: body.SourceBranch,
|
||||
TargetBranch: body.TargetBranch,
|
||||
Status: models.PRStatusOpen,
|
||||
}
|
||||
if _, err := h.db.Insert(pr); err != nil {
|
||||
jsonError(w, "could not create pull request", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "created"})
|
||||
json.NewEncoder(w).Encode(pr)
|
||||
}
|
||||
|
||||
func (h *PRHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
pr, ok := h.lookupPR(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
jsonOK(w, pr)
|
||||
}
|
||||
|
||||
func (h *PRHandler) Merge(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "merged"})
|
||||
pr, ok := h.lookupPR(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if pr.Status != models.PRStatusOpen {
|
||||
jsonError(w, "pull request is not open", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
pr.Status = models.PRStatusMerged
|
||||
if _, err := h.db.ID(pr.ID).Cols("status").Update(pr); err != nil {
|
||||
jsonError(w, "could not merge pull request", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
jsonOK(w, pr)
|
||||
}
|
||||
|
||||
func (h *PRHandler) Close(w http.ResponseWriter, r *http.Request) {
|
||||
pr, ok := h.lookupPR(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if pr.Status != models.PRStatusOpen {
|
||||
jsonError(w, "pull request is not open", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
pr.Status = models.PRStatusClosed
|
||||
if _, err := h.db.ID(pr.ID).Cols("status").Update(pr); err != nil {
|
||||
jsonError(w, "could not close pull request", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
jsonOK(w, pr)
|
||||
}
|
||||
|
||||
func (h *PRHandler) repoIDFromURL(w http.ResponseWriter, r *http.Request) (int64, bool) {
|
||||
ownerName := chi.URLParam(r, "owner")
|
||||
repoName := chi.URLParam(r, "repo")
|
||||
|
||||
var owner models.User
|
||||
found, err := h.db.Where("username = ?", ownerName).Get(&owner)
|
||||
if err != nil || !found {
|
||||
jsonError(w, "repository not found", http.StatusNotFound)
|
||||
return 0, false
|
||||
}
|
||||
|
||||
var repo models.Repository
|
||||
found, err = h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo)
|
||||
if err != nil || !found {
|
||||
jsonError(w, "repository not found", http.StatusNotFound)
|
||||
return 0, false
|
||||
}
|
||||
return repo.ID, true
|
||||
}
|
||||
|
||||
func (h *PRHandler) lookupPR(w http.ResponseWriter, r *http.Request) (*models.PullRequest, bool) {
|
||||
repoID, ok := h.repoIDFromURL(w, r)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
prIDStr := chi.URLParam(r, "prID")
|
||||
prID, err := strconv.ParseInt(prIDStr, 10, 64)
|
||||
if err != nil {
|
||||
jsonError(w, "invalid pull request ID", http.StatusBadRequest)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var pr models.PullRequest
|
||||
found, err := h.db.Where("id = ? AND repo_id = ?", prID, repoID).Get(&pr)
|
||||
if err != nil || !found {
|
||||
jsonError(w, "pull request not found", http.StatusNotFound)
|
||||
return nil, false
|
||||
}
|
||||
return &pr, true
|
||||
}
|
||||
|
||||
+175
-12
@@ -3,36 +3,199 @@ package handlers
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/forgeo/forgebucket/internal/api/middleware"
|
||||
"github.com/forgeo/forgebucket/internal/config"
|
||||
gitdomain "github.com/forgeo/forgebucket/internal/domain/git"
|
||||
"github.com/forgeo/forgebucket/internal/models"
|
||||
)
|
||||
|
||||
type RepoHandler struct{}
|
||||
type RepoHandler struct {
|
||||
db *xorm.Engine
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewRepoHandler() *RepoHandler {
|
||||
return &RepoHandler{}
|
||||
func NewRepoHandler(db *xorm.Engine, cfg *config.Config) *RepoHandler {
|
||||
return &RepoHandler{db: db, cfg: cfg}
|
||||
}
|
||||
|
||||
func (h *RepoHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode([]any{})
|
||||
userID, _ := middleware.UserIDFromContext(r.Context())
|
||||
|
||||
var repos []models.Repository
|
||||
if err := h.db.Where("owner_id = ?", userID).Find(&repos); err != nil {
|
||||
jsonError(w, "could not list repositories", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if repos == nil {
|
||||
repos = []models.Repository{}
|
||||
}
|
||||
jsonOK(w, repos)
|
||||
}
|
||||
|
||||
func (h *RepoHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _ := middleware.UserIDFromContext(r.Context())
|
||||
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsPrivate bool `json:"isPrivate"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
jsonError(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.Name == "" {
|
||||
jsonError(w, "name is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
diskPath := filepath.Join(h.cfg.RepoRoot, strconv.FormatInt(userID, 10), body.Name+".git")
|
||||
|
||||
repo := &models.Repository{
|
||||
OwnerID: userID,
|
||||
Name: body.Name,
|
||||
Description: body.Description,
|
||||
IsPrivate: body.IsPrivate,
|
||||
DefaultBranch: "main",
|
||||
DiskPath: diskPath,
|
||||
}
|
||||
|
||||
// Initialise bare repo on disk before inserting to DB
|
||||
gitdomain.SetRepoRoot(h.cfg.RepoRoot)
|
||||
if err := gitdomain.Init(diskPath); err != nil {
|
||||
jsonError(w, "could not initialise git repository", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := h.db.Insert(repo); err != nil {
|
||||
jsonError(w, "repository name already taken", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "created"})
|
||||
json.NewEncoder(w).Encode(repo)
|
||||
}
|
||||
|
||||
func (h *RepoHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
repo, ok := h.lookupRepo(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
jsonOK(w, repo)
|
||||
}
|
||||
|
||||
func (h *RepoHandler) Tree(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode([]any{})
|
||||
repo, ok := h.lookupRepo(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ref := r.URL.Query().Get("ref")
|
||||
if ref == "" {
|
||||
ref = repo.DefaultBranch
|
||||
}
|
||||
subPath := r.URL.Query().Get("path")
|
||||
|
||||
gitdomain.SetRepoRoot(h.cfg.RepoRoot)
|
||||
entries, err := gitdomain.TreeLS(repo.DiskPath, ref, subPath)
|
||||
if err != nil {
|
||||
jsonError(w, "could not read tree", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if entries == nil {
|
||||
entries = []gitdomain.TreeEntry{}
|
||||
}
|
||||
jsonOK(w, entries)
|
||||
}
|
||||
|
||||
func (h *RepoHandler) Blob(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"content": ""})
|
||||
repo, ok := h.lookupRepo(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ref := r.URL.Query().Get("ref")
|
||||
if ref == "" {
|
||||
ref = repo.DefaultBranch
|
||||
}
|
||||
path := r.URL.Query().Get("path")
|
||||
if path == "" {
|
||||
jsonError(w, "path is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
gitdomain.SetRepoRoot(h.cfg.RepoRoot)
|
||||
content, err := gitdomain.BlobCat(repo.DiskPath, ref, path)
|
||||
if err != nil {
|
||||
jsonError(w, "file not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
jsonOK(w, map[string]string{"content": string(content), "path": path, "ref": ref})
|
||||
}
|
||||
|
||||
func (h *RepoHandler) Commits(w http.ResponseWriter, r *http.Request) {
|
||||
repo, ok := h.lookupRepo(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ref := r.URL.Query().Get("ref")
|
||||
if ref == "" {
|
||||
ref = repo.DefaultBranch
|
||||
}
|
||||
limit := 30
|
||||
if l := r.URL.Query().Get("limit"); l != "" {
|
||||
if n, err := strconv.Atoi(l); err == nil && n > 0 && n <= 100 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
|
||||
gitdomain.SetRepoRoot(h.cfg.RepoRoot)
|
||||
commits, err := gitdomain.Log(repo.DiskPath, ref, limit)
|
||||
if err != nil {
|
||||
jsonError(w, "could not read commits", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if commits == nil {
|
||||
commits = []gitdomain.Commit{}
|
||||
}
|
||||
jsonOK(w, commits)
|
||||
}
|
||||
|
||||
// lookupRepo resolves {owner}/{repo} URL params to a DB row, enforcing access.
|
||||
func (h *RepoHandler) lookupRepo(w http.ResponseWriter, r *http.Request) (*models.Repository, bool) {
|
||||
ownerName := chi.URLParam(r, "owner")
|
||||
repoName := chi.URLParam(r, "repo")
|
||||
|
||||
var owner models.User
|
||||
found, err := h.db.Where("username = ?", ownerName).Get(&owner)
|
||||
if err != nil || !found {
|
||||
jsonError(w, "repository not found", http.StatusNotFound)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var repo models.Repository
|
||||
found, err = h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo)
|
||||
if err != nil || !found {
|
||||
jsonError(w, "repository not found", http.StatusNotFound)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Private repo: only the owner may access (RBAC will be expanded in Phase 3)
|
||||
if repo.IsPrivate {
|
||||
callerID, _ := middleware.UserIDFromContext(r.Context())
|
||||
if callerID != repo.OwnerID {
|
||||
jsonError(w, "repository not found", http.StatusNotFound)
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
return &repo, true
|
||||
}
|
||||
|
||||
@@ -5,24 +5,91 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/forgeo/forgebucket/internal/api/middleware"
|
||||
"github.com/forgeo/forgebucket/internal/models"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
db *xorm.Engine
|
||||
store sessions.Store
|
||||
}
|
||||
|
||||
func NewUserHandler(store sessions.Store) *UserHandler {
|
||||
return &UserHandler{store: store}
|
||||
func NewUserHandler(db *xorm.Engine, store sessions.Store) *UserHandler {
|
||||
return &UserHandler{db: db, store: store}
|
||||
}
|
||||
|
||||
func (h *UserHandler) Me(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
jsonError(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.Username == "" || body.Email == "" || body.Password == "" {
|
||||
jsonError(w, "username, email, and password are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(body.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
jsonError(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
user := &models.User{
|
||||
Username: body.Username,
|
||||
Email: body.Email,
|
||||
PasswordHash: string(hash),
|
||||
}
|
||||
if _, err := h.db.Insert(user); err != nil {
|
||||
jsonError(w, "username or email already taken", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
func (h *UserHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
jsonError(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
found, err := h.db.Where("username = ?", body.Username).Get(&user)
|
||||
if err != nil || !found {
|
||||
jsonError(w, "invalid credentials", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(body.Password)); err != nil {
|
||||
jsonError(w, "invalid credentials", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
session, _ := h.store.Get(r, "fb_session")
|
||||
session.Values["userID"] = user.ID
|
||||
session.Values["username"] = user.Username
|
||||
session.Values["isAdmin"] = user.IsAdmin
|
||||
if err := session.Save(r, w); err != nil {
|
||||
jsonError(w, "could not save session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
func (h *UserHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -31,3 +98,21 @@ func (h *UserHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
session.Save(r, w)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *UserHandler) Me(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.UserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
jsonError(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
found, err := h.db.ID(userID).Get(&user)
|
||||
if err != nil || !found {
|
||||
jsonError(w, "user not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user