You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1054 lines
26 KiB

package handlers
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"html/template"
"io/fs"
"log"
"net/http"
"strconv"
"strings"
"sync"
"teraclone"
"teraclone/internal/config"
"teraclone/internal/mock"
"teraclone/internal/service"
"teraclone/internal/store"
)
type DeviceHandler struct {
templates *template.Template
static http.Handler
app *service.AppService
sessions map[string]string
mu sync.RWMutex
}
type apiResponse struct {
OK bool `json:"ok"`
Message string `json:"message"`
Mock bool `json:"mock"`
Data interface{} `json:"data,omitempty"`
}
type loginPageData struct {
AppName string
Error string
}
const sessionCookieName = "teraclone_session"
func NewDeviceHandler(appService *service.AppService) (*DeviceHandler, error) {
webRoot, err := teraclone.EmbeddedWebRoot()
if err != nil {
return nil, err
}
tmpl, err := template.New("").Funcs(template.FuncMap{
"eq": func(a, b string) bool { return a == b },
"contains": func(items []string, target string) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
},
}).ParseFS(webRoot, "templates/*.html")
if err != nil {
return nil, err
}
if _, err := tmpl.ParseFS(webRoot, "templates/partials/*.html"); err != nil {
return nil, err
}
if _, err := tmpl.ParseFS(webRoot, "templates/auth/*.html"); err != nil {
return nil, err
}
staticRoot, err := fs.Sub(webRoot, "static")
if err != nil {
return nil, err
}
return &DeviceHandler{
templates: tmpl,
static: http.FileServer(http.FS(staticRoot)),
app: appService,
sessions: make(map[string]string),
}, nil
}
func (h *DeviceHandler) RegisterRoutes(mux *http.ServeMux) {
mux.Handle("/static/", http.StripPrefix("/static/", h.static))
mux.HandleFunc("/login", h.handleLogin)
mux.HandleFunc("/logout", h.handleLogout)
mux.HandleFunc("/api/status", h.handleAPIStatus)
mux.HandleFunc("/api/network", h.handleAPINetwork)
mux.HandleFunc("/api/network/apply", h.handleMockPost("네트워크 설정 모의 적용이 완료되었습니다."))
mux.HandleFunc("/api/system", h.handleAPISystem)
mux.HandleFunc("/api/system/reboot", h.handleMockPost("장치 재시작 요청을 모의 처리했습니다."))
mux.HandleFunc("/api/logs/system", h.handleAPISystemLogs)
mux.HandleFunc("/api/logs/ports", h.handleAPIPortLogs)
mux.HandleFunc("/api/activity/recent", h.handleRecentActivity)
mux.HandleFunc("/api/users", h.handleUsers)
mux.HandleFunc("/api/users/", h.handleUserByID)
mux.HandleFunc("/api/groups", h.handleGroups)
mux.HandleFunc("/api/groups/", h.handleGroupByID)
mux.HandleFunc("/api/mock/action", h.handleMockAction)
mux.HandleFunc("/", h.handlePage)
}
func (h *DeviceHandler) handlePage(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.NotFound(w, r)
return
}
username, ok := h.requirePageAuth(w, r)
if !ok {
return
}
if r.URL.Path == "/admin/users" {
h.handleUsersPage(w, r, username)
return
}
if r.URL.Path == "/admin/groups" {
h.handleGroupsPage(w, r, username)
return
}
page, ok := mock.ResolvePage(r.URL.Path)
if !ok {
http.NotFound(w, r)
return
}
page.CurrentUser = username
if err := h.app.RecordPageVisit(r.URL.Path, page.Title); err != nil {
log.Printf("sqlite page visit error: %v", err)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = h.templates.ExecuteTemplate(w, "layout", page)
}
func (h *DeviceHandler) handleUsersPage(w http.ResponseWriter, r *http.Request, username string) {
page, ok := mock.ResolvePage(r.URL.Path)
if !ok {
http.NotFound(w, r)
return
}
users, err := h.app.ListUsers()
if err != nil {
http.Error(w, "failed to load users", http.StatusInternalServerError)
return
}
groups, err := h.app.ListGroups()
if err != nil {
http.Error(w, "failed to load groups", http.StatusInternalServerError)
return
}
page.Blocks = buildUserBlocksWithGroups(users, groups)
page.CurrentUser = username
if err := h.app.RecordPageVisit(r.URL.Path, page.Title); err != nil {
log.Printf("sqlite page visit error: %v", err)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := h.templates.ExecuteTemplate(w, "layout", page); err != nil {
log.Printf("template execute error: %v", err)
}
}
func (h *DeviceHandler) handleGroupsPage(w http.ResponseWriter, r *http.Request, username string) {
page, ok := mock.ResolvePage(r.URL.Path)
if !ok {
http.NotFound(w, r)
return
}
groups, err := h.app.ListGroups()
if err != nil {
http.Error(w, "failed to load groups", http.StatusInternalServerError)
return
}
page.Blocks = buildGroupBlocks(groups)
page.CurrentUser = username
if err := h.app.RecordPageVisit(r.URL.Path, page.Title); err != nil {
log.Printf("sqlite page visit error: %v", err)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := h.templates.ExecuteTemplate(w, "layout", page); err != nil {
log.Printf("template execute error: %v", err)
}
}
func (h *DeviceHandler) handleLogin(w http.ResponseWriter, r *http.Request) {
if currentUser, ok := h.authenticatedUser(r); ok {
http.Redirect(w, r, redirectTarget(r, "/"), http.StatusSeeOther)
_ = currentUser
return
}
switch r.Method {
case http.MethodGet:
h.renderLogin(w, "")
case http.MethodPost:
username := strings.TrimSpace(r.FormValue("username"))
password := r.FormValue("password")
user, err := h.app.AuthenticateUser(username, password)
if err != nil {
h.renderLogin(w, "아이디 또는 비밀번호가 올바르지 않습니다.")
return
}
token, err := generateSessionToken()
if err != nil {
http.Error(w, "failed to create session", http.StatusInternalServerError)
return
}
h.mu.Lock()
h.sessions[token] = user.Username
h.mu.Unlock()
http.SetCookie(w, &http.Cookie{
Name: sessionCookieName,
Value: token,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
http.Redirect(w, r, "/", http.StatusSeeOther)
default:
http.NotFound(w, r)
}
}
func (h *DeviceHandler) handleLogout(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.NotFound(w, r)
return
}
if cookie, err := r.Cookie(sessionCookieName); err == nil && cookie.Value != "" {
h.mu.Lock()
delete(h.sessions, cookie.Value)
h.mu.Unlock()
}
http.SetCookie(w, &http.Cookie{
Name: sessionCookieName,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
func (h *DeviceHandler) renderLogin(w http.ResponseWriter, errorMessage string) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := h.templates.ExecuteTemplate(w, "login", loginPageData{
AppName: "TERACLONE",
Error: errorMessage,
}); err != nil {
http.Error(w, "failed to render login", http.StatusInternalServerError)
}
}
func (h *DeviceHandler) requirePageAuth(w http.ResponseWriter, r *http.Request) (string, bool) {
username, ok := h.authenticatedUser(r)
if ok {
return username, true
}
if r.URL.Path == "/login" {
return "", true
}
http.Redirect(w, r, redirectTarget(r, "/login"), http.StatusSeeOther)
return "", false
}
func (h *DeviceHandler) requireAPIAuth(w http.ResponseWriter, r *http.Request) (string, bool) {
username, ok := h.authenticatedUser(r)
if ok {
return username, true
}
writeJSON(w, http.StatusUnauthorized, apiResponse{
OK: false,
Message: "로그인이 필요합니다.",
Mock: false,
})
return "", false
}
func (h *DeviceHandler) authenticatedUser(r *http.Request) (string, bool) {
cookie, err := r.Cookie(sessionCookieName)
if err != nil || cookie.Value == "" {
return "", false
}
h.mu.RLock()
username, ok := h.sessions[cookie.Value]
h.mu.RUnlock()
return username, ok
}
func (h *DeviceHandler) handleAPIStatus(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireAPIAuth(w, r); !ok {
return
}
writeJSON(w, http.StatusOK, apiResponse{
OK: true,
Message: "상태 더미 데이터를 반환했습니다.",
Mock: config.MockMode,
Data: mock.StatusAPIData(),
})
}
func (h *DeviceHandler) handleAPINetwork(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireAPIAuth(w, r); !ok {
return
}
writeJSON(w, http.StatusOK, apiResponse{
OK: true,
Message: "네트워크 더미 데이터를 반환했습니다.",
Mock: config.MockMode,
Data: mock.NetworkAPIData(),
})
}
func (h *DeviceHandler) handleAPISystem(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireAPIAuth(w, r); !ok {
return
}
writeJSON(w, http.StatusOK, apiResponse{
OK: true,
Message: "시스템 더미 데이터를 반환했습니다.",
Mock: config.MockMode,
Data: mock.SystemAPIData(),
})
}
func (h *DeviceHandler) handleAPISystemLogs(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireAPIAuth(w, r); !ok {
return
}
writeJSON(w, http.StatusOK, apiResponse{
OK: true,
Message: "시스템 로그 더미 데이터를 반환했습니다.",
Mock: config.MockMode,
Data: mock.SystemLogsAPIData(),
})
}
func (h *DeviceHandler) handleAPIPortLogs(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireAPIAuth(w, r); !ok {
return
}
writeJSON(w, http.StatusOK, apiResponse{
OK: true,
Message: "포트 로그 더미 데이터를 반환했습니다.",
Mock: config.MockMode,
Data: mock.PortLogsAPIData(),
})
}
func (h *DeviceHandler) handleRecentActivity(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireAPIAuth(w, r); !ok {
return
}
records, err := h.app.RecentActivity(30)
if err != nil {
writeJSON(w, http.StatusInternalServerError, apiResponse{
OK: false,
Message: "최근 활동 조회에 실패했습니다.",
Mock: config.MockMode,
})
return
}
writeJSON(w, http.StatusOK, apiResponse{
OK: true,
Message: "최근 활동을 조회했습니다.",
Mock: config.MockMode,
Data: records,
})
}
func (h *DeviceHandler) handleMockAction(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireAPIAuth(w, r); !ok {
return
}
if r.Method != http.MethodPost {
http.NotFound(w, r)
return
}
action := r.FormValue("action")
if action == "" {
action = "mock-action"
}
if err := h.app.RecordMockAction(action, r.Referer()); err != nil {
log.Printf("sqlite mock action error: %v", err)
}
message := "모의 동작을 처리했습니다."
switch {
case strings.Contains(action, "save"):
message = "모의 저장이 완료되었습니다."
case strings.Contains(action, "apply"):
message = "모의 적용이 완료되었습니다."
case strings.Contains(action, "reboot"):
message = "모의 재시작 요청이 접수되었습니다."
case strings.Contains(action, "download"):
message = "모의 다운로드 요청을 처리했습니다."
case strings.Contains(action, "refresh"):
message = "모의 새로고침이 완료되었습니다."
case strings.Contains(action, "delete"):
message = "모의 삭제가 완료되었습니다."
case strings.Contains(action, "add"):
message = "모의 추가가 완료되었습니다."
}
writeJSON(w, http.StatusOK, apiResponse{
OK: true,
Message: message,
Mock: config.MockMode,
Data: map[string]string{
"action": action,
},
})
}
func (h *DeviceHandler) handleMockPost(message string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireAPIAuth(w, r); !ok {
return
}
if r.Method != http.MethodPost {
http.NotFound(w, r)
return
}
writeJSON(w, http.StatusOK, apiResponse{
OK: true,
Message: message,
Mock: config.MockMode,
})
}
}
func (h *DeviceHandler) handleUsers(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireAPIAuth(w, r); !ok {
return
}
switch r.Method {
case http.MethodGet:
users, err := h.app.ListUsers()
if err != nil {
writeJSON(w, http.StatusInternalServerError, apiResponse{
OK: false,
Message: "사용자 목록을 불러오지 못했습니다.",
Mock: false,
})
return
}
writeJSON(w, http.StatusOK, apiResponse{
OK: true,
Message: "사용자 목록을 조회했습니다.",
Mock: false,
Data: users,
})
case http.MethodPost:
var input service.CreateUserInput
if err := decodeUserPayload(r, &input); err != nil {
writeJSON(w, http.StatusBadRequest, apiResponse{
OK: false,
Message: err.Error(),
Mock: false,
})
return
}
user, err := h.app.CreateUser(input)
if err != nil {
writeUserError(w, err)
return
}
_ = h.app.RecordMockAction("create-user", "/admin/users")
writeJSON(w, http.StatusCreated, apiResponse{
OK: true,
Message: "사용자를 추가했습니다.",
Mock: false,
Data: user,
})
default:
http.NotFound(w, r)
}
}
func (h *DeviceHandler) handleUserByID(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireAPIAuth(w, r); !ok {
return
}
id, err := parseUserID(r.URL.Path)
if err != nil {
http.NotFound(w, r)
return
}
switch r.Method {
case http.MethodGet:
user, err := h.app.GetUser(id)
if err != nil {
writeUserError(w, err)
return
}
writeJSON(w, http.StatusOK, apiResponse{
OK: true,
Message: "사용자 정보를 조회했습니다.",
Mock: false,
Data: user,
})
case http.MethodPut:
var input service.UpdateUserInput
if err := decodeUserPayload(r, &input); err != nil {
writeJSON(w, http.StatusBadRequest, apiResponse{
OK: false,
Message: err.Error(),
Mock: false,
})
return
}
input.ID = id
user, err := h.app.UpdateUser(input)
if err != nil {
writeUserError(w, err)
return
}
_ = h.app.RecordMockAction("update-user", "/admin/users")
writeJSON(w, http.StatusOK, apiResponse{
OK: true,
Message: "사용자 정보를 수정했습니다.",
Mock: false,
Data: user,
})
case http.MethodDelete:
if err := h.app.DeleteUser(id); err != nil {
writeUserError(w, err)
return
}
_ = h.app.RecordMockAction("delete-user", "/admin/users")
writeJSON(w, http.StatusOK, apiResponse{
OK: true,
Message: "사용자를 삭제했습니다.",
Mock: false,
})
default:
http.NotFound(w, r)
}
}
func (h *DeviceHandler) handleGroups(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireAPIAuth(w, r); !ok {
return
}
switch r.Method {
case http.MethodGet:
groups, err := h.app.ListGroups()
if err != nil {
writeJSON(w, http.StatusInternalServerError, apiResponse{
OK: false,
Message: "그룹 목록을 불러오지 못했습니다.",
Mock: false,
})
return
}
writeJSON(w, http.StatusOK, apiResponse{
OK: true,
Message: "그룹 목록을 조회했습니다.",
Mock: false,
Data: groups,
})
case http.MethodPost:
var input service.CreateGroupInput
if err := decodeGroupPayload(r, &input); err != nil {
writeJSON(w, http.StatusBadRequest, apiResponse{
OK: false,
Message: err.Error(),
Mock: false,
})
return
}
group, err := h.app.CreateGroup(input)
if err != nil {
writeGroupError(w, err)
return
}
_ = h.app.RecordMockAction("create-group", "/admin/groups")
writeJSON(w, http.StatusCreated, apiResponse{
OK: true,
Message: "그룹을 추가했습니다.",
Mock: false,
Data: group,
})
default:
http.NotFound(w, r)
}
}
func (h *DeviceHandler) handleGroupByID(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireAPIAuth(w, r); !ok {
return
}
id, err := parseGroupID(r.URL.Path)
if err != nil {
http.NotFound(w, r)
return
}
switch r.Method {
case http.MethodGet:
group, err := h.app.GetGroup(id)
if err != nil {
writeGroupError(w, err)
return
}
writeJSON(w, http.StatusOK, apiResponse{
OK: true,
Message: "그룹 정보를 조회했습니다.",
Mock: false,
Data: group,
})
case http.MethodPut:
var input service.UpdateGroupInput
if err := decodeGroupPayload(r, &input); err != nil {
writeJSON(w, http.StatusBadRequest, apiResponse{
OK: false,
Message: err.Error(),
Mock: false,
})
return
}
input.ID = id
group, err := h.app.UpdateGroup(input)
if err != nil {
writeGroupError(w, err)
return
}
_ = h.app.RecordMockAction("update-group", "/admin/groups")
writeJSON(w, http.StatusOK, apiResponse{
OK: true,
Message: "그룹 정보를 수정했습니다.",
Mock: false,
Data: group,
})
case http.MethodDelete:
if err := h.app.DeleteGroup(id); err != nil {
writeGroupError(w, err)
return
}
_ = h.app.RecordMockAction("delete-group", "/admin/groups")
writeJSON(w, http.StatusOK, apiResponse{
OK: true,
Message: "그룹을 삭제했습니다.",
Mock: false,
})
default:
http.NotFound(w, r)
}
}
func buildUserBlocks(users []service.User) []mock.ContentBlock {
rows := make([]mock.TableRow, 0, len(users))
for _, user := range users {
rows = append(rows, mock.TableRow{
Cells: []string{
user.StatusLabel,
user.Username,
user.Group,
user.AccessSummary,
},
Actions: []mock.RowAction{
{Label: "수정", Action: "user-edit", Variant: "primary", Target: strconv.FormatInt(user.ID, 10)},
{Label: "삭제", Action: "user-delete", Variant: "danger", Target: strconv.FormatInt(user.ID, 10)},
},
})
}
return []mock.ContentBlock{
{
Kind: "table",
Title: "사용자 계정",
Table: &mock.TableData{
Columns: []string{"상태", "사용자 이름", "그룹", "포트 권한"},
Rows: rows,
EmptyMessage: "등록된 사용자가 없습니다.",
PrimaryLabel: "사용자 추가",
},
},
{
Kind: "modal",
Title: "사용자 추가",
Modal: &mock.ModalData{
Root: "user-create",
Title: "사용자 추가",
Message: "사용자 생성과 수정을 같은 팝업에서 처리합니다.",
Fields: []mock.FormField{
{Type: "hidden", Name: "id", Value: ""},
{Label: "사용자 이름", Name: "username", Type: "text", Value: "", Required: true},
{Label: "비밀번호", Name: "password", Type: "password", Value: "", Placeholder: "수정 시 비우면 유지됩니다."},
{
Label: "그룹",
Name: "group",
Type: "select",
Value: "administrator",
Options: []mock.Option{
{Label: "administrator", Value: "administrator", Selected: true},
{Label: "operator", Value: "operator"},
{Label: "guest", Value: "guest"},
},
},
{
Label: "상태",
Name: "enabled",
Type: "select",
Value: "true",
Options: []mock.Option{
{Label: "사용", Value: "true", Selected: true},
{Label: "중지", Value: "false"},
},
},
},
Buttons: []mock.ActionButton{
{Label: "확인", Action: "submit-user-form", Variant: "primary"},
{Label: "취소", Action: "cancel-modal", Variant: "secondary"},
},
},
},
}
}
func buildUserBlocksWithGroups(users []service.User, groups []service.Group) []mock.ContentBlock {
groupOptions := make([]mock.Option, 0, len(groups))
accessByGroup := make(map[string]string, len(groups))
defaultGroup := ""
for index, group := range groups {
if defaultGroup == "" || group.Name == "administrator" {
defaultGroup = group.Name
}
groupOptions = append(groupOptions, mock.Option{
Label: group.Name,
Value: group.Name,
Selected: index == 0 || group.Name == "administrator",
})
accessByGroup[group.Name] = group.PermissionSummary
}
rows := make([]mock.TableRow, 0, len(users))
for _, user := range users {
accessSummary := user.AccessSummary
if summary, ok := accessByGroup[user.Group]; ok {
accessSummary = summary
}
rows = append(rows, mock.TableRow{
Cells: []string{
user.StatusLabel,
user.Username,
user.Group,
accessSummary,
},
Actions: []mock.RowAction{
{Label: "수정", Action: "user-edit", Variant: "primary", Target: strconv.FormatInt(user.ID, 10)},
{Label: "삭제", Action: "user-delete", Variant: "danger", Target: strconv.FormatInt(user.ID, 10)},
},
})
}
return []mock.ContentBlock{
{
Kind: "table",
Title: "사용자 계정",
Table: &mock.TableData{
Columns: []string{"상태", "사용자 이름", "그룹", "포트 권한"},
Rows: rows,
EmptyMessage: "등록된 사용자가 없습니다.",
PrimaryLabel: "사용자 추가",
},
},
{
Kind: "modal",
Title: "사용자 추가",
Modal: &mock.ModalData{
Root: "user-create",
Title: "사용자 추가",
Message: "사용자 생성과 수정은 같은 팝업에서 처리합니다.",
Fields: []mock.FormField{
{Type: "hidden", Name: "id", Value: ""},
{Label: "사용자 이름", Name: "username", Type: "text", Value: "", Required: true},
{Label: "비밀번호", Name: "password", Type: "password", Value: "", Placeholder: "수정 시 비우면 유지됩니다."},
{
Label: "그룹",
Name: "group",
Type: "select",
Value: defaultGroup,
Options: groupOptions,
},
{
Label: "상태",
Name: "enabled",
Type: "select",
Value: "true",
Options: []mock.Option{
{Label: "사용", Value: "true", Selected: true},
{Label: "중지", Value: "false"},
},
},
},
Buttons: []mock.ActionButton{
{Label: "확인", Action: "submit-user-form", Variant: "primary"},
{Label: "취소", Action: "cancel-modal", Variant: "secondary"},
},
},
},
}
}
func buildGroupBlocks(groups []service.Group) []mock.ContentBlock {
rows := make([]mock.TableRow, 0, len(groups))
for _, group := range groups {
rows = append(rows, mock.TableRow{
Cells: []string{
group.Name,
group.Description,
group.PermissionSummary,
},
Actions: []mock.RowAction{
{Label: "수정", Action: "group-edit", Variant: "primary", Target: strconv.FormatInt(group.ID, 10)},
{Label: "삭제", Action: "group-delete", Variant: "danger", Target: strconv.FormatInt(group.ID, 10)},
},
})
}
return []mock.ContentBlock{
{
Kind: "table",
Title: "그룹",
Table: &mock.TableData{
Columns: []string{"그룹 이름", "설명", "권한"},
Rows: rows,
EmptyMessage: "등록된 그룹이 없습니다.",
PrimaryLabel: "그룹 추가",
},
},
}
}
func parseUserID(path string) (int64, error) {
idText := strings.TrimPrefix(path, "/api/users/")
if idText == "" || strings.Contains(idText, "/") {
return 0, errors.New("invalid user id")
}
return strconv.ParseInt(idText, 10, 64)
}
func parseGroupID(path string) (int64, error) {
idText := strings.TrimPrefix(path, "/api/groups/")
if idText == "" || strings.Contains(idText, "/") {
return 0, errors.New("invalid group id")
}
return strconv.ParseInt(idText, 10, 64)
}
func decodeUserPayload(r *http.Request, dest interface{}) error {
contentType := r.Header.Get("Content-Type")
if strings.Contains(contentType, "application/json") {
return json.NewDecoder(r.Body).Decode(dest)
}
if err := r.ParseForm(); err != nil {
return err
}
switch payload := dest.(type) {
case *service.CreateUserInput:
payload.Username = strings.TrimSpace(r.FormValue("username"))
payload.Password = r.FormValue("password")
payload.Group = strings.TrimSpace(r.FormValue("group"))
payload.Enabled = parseEnabledValue(r.FormValue("enabled"))
case *service.UpdateUserInput:
payload.Username = strings.TrimSpace(r.FormValue("username"))
payload.Password = r.FormValue("password")
payload.Group = strings.TrimSpace(r.FormValue("group"))
payload.Enabled = parseEnabledValue(r.FormValue("enabled"))
default:
return errors.New("unsupported payload")
}
return nil
}
func decodeGroupPayload(r *http.Request, dest interface{}) error {
contentType := r.Header.Get("Content-Type")
if strings.Contains(contentType, "application/json") {
return json.NewDecoder(r.Body).Decode(dest)
}
if err := r.ParseForm(); err != nil {
return err
}
permissions := append([]string(nil), r.Form["permissions"]...)
switch payload := dest.(type) {
case *service.CreateGroupInput:
payload.Name = strings.TrimSpace(r.FormValue("name"))
payload.Description = strings.TrimSpace(r.FormValue("description"))
payload.Permissions = permissions
case *service.UpdateGroupInput:
payload.Name = strings.TrimSpace(r.FormValue("name"))
payload.Description = strings.TrimSpace(r.FormValue("description"))
payload.Permissions = permissions
default:
return errors.New("unsupported payload")
}
return nil
}
func parseEnabledValue(value string) bool {
switch strings.ToLower(strings.TrimSpace(value)) {
case "false", "0", "off", "disabled":
return false
default:
return true
}
}
func writeUserError(w http.ResponseWriter, err error) {
switch {
case err == nil:
return
case store.IsNotFoundError(err):
writeJSON(w, http.StatusNotFound, apiResponse{
OK: false,
Message: "사용자를 찾을 수 없습니다.",
Mock: false,
})
case store.IsUniqueConstraintError(err):
writeJSON(w, http.StatusConflict, apiResponse{
OK: false,
Message: "같은 사용자 이름이 이미 존재합니다.",
Mock: false,
})
default:
writeJSON(w, http.StatusBadRequest, apiResponse{
OK: false,
Message: err.Error(),
Mock: false,
})
}
}
func writeGroupError(w http.ResponseWriter, err error) {
switch {
case err == nil:
return
case store.IsNotFoundError(err):
writeJSON(w, http.StatusNotFound, apiResponse{
OK: false,
Message: "그룹을 찾을 수 없습니다.",
Mock: false,
})
case store.IsUniqueConstraintError(err):
writeJSON(w, http.StatusConflict, apiResponse{
OK: false,
Message: "같은 그룹 이름이 이미 존재합니다.",
Mock: false,
})
case store.IsDependencyError(err):
writeJSON(w, http.StatusConflict, apiResponse{
OK: false,
Message: err.Error(),
Mock: false,
})
default:
writeJSON(w, http.StatusBadRequest, apiResponse{
OK: false,
Message: err.Error(),
Mock: false,
})
}
}
func generateSessionToken() (string, error) {
buffer := make([]byte, 32)
if _, err := rand.Read(buffer); err != nil {
return "", err
}
return hex.EncodeToString(buffer), nil
}
func redirectTarget(r *http.Request, fallback string) string {
if next := strings.TrimSpace(r.URL.Query().Get("next")); next != "" && strings.HasPrefix(next, "/") {
return next
}
return fallback
}
func writeJSON(w http.ResponseWriter, status int, payload apiResponse) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}