first commit

This commit is contained in:
hayato5246
2026-03-31 19:32:59 +09:00
commit d10b794c9f
78 changed files with 1671595 additions and 0 deletions

100
middleware/auth.go Normal file
View File

@@ -0,0 +1,100 @@
package middleware
import (
"context"
"net/http"
"stocksearch/services"
"strings"
)
const SessionCookieName = "ss_session"
// contextKey 컨텍스트 키 타입
type contextKey string
const CtxLoggedIn contextKey = "loggedIn"
// IsLoggedIn 요청 컨텍스트에서 로그인 여부 반환
func IsLoggedIn(r *http.Request) bool {
v, _ := r.Context().Value(CtxLoggedIn).(bool)
return v
}
// publicPaths 로그인 없이 접근 가능한 경로 (전체 일치 또는 prefix)
var publicPaths = []string{
"/login",
"/static/",
// 공개 페이지
"/",
"/theme",
"/kospi200",
"/stock/",
// 공개 API (시세/테마/코스피200 관련)
"/api/stock/",
"/api/indices",
"/api/search",
"/api/scanner/",
"/api/signal",
"/api/watchlist-signal",
"/api/themes",
"/api/themes/",
"/api/kospi200",
"/api/news",
"/api/disclosure",
"/ws",
}
// isPublic 요청 경로가 공개 경로에 해당하는지 판단
func isPublic(path string) bool {
for _, p := range publicPaths {
if strings.HasSuffix(p, "/") {
if strings.HasPrefix(path, p) || path == p[:len(p)-1] {
return true
}
} else {
if path == p {
return true
}
}
}
return false
}
// Auth 세션 쿠키 검증 미들웨어
// - 공개 경로: 로그인 없이 접근 허용
// - 인증 필요 경로(자산/주문/자동매매 등): 미인증 시 페이지는 /login 리다이렉트, API는 401 반환
func Auth(sessionSvc *services.SessionService) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 공개 경로: 인증 없이 통과하되 로그인 상태는 컨텍스트에 저장
if isPublic(r.URL.Path) {
cookie, err := r.Cookie(SessionCookieName)
loggedIn := err == nil && sessionSvc.Validate(cookie.Value)
ctx := context.WithValue(r.Context(), CtxLoggedIn, loggedIn)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
// 쿠키에서 세션 ID 조회
cookie, err := r.Cookie(SessionCookieName)
loggedIn := err == nil && sessionSvc.Validate(cookie.Value)
if !loggedIn {
// API 경로는 401 JSON 반환, 페이지는 /login 리다이렉트
if strings.HasPrefix(r.URL.Path, "/api/") {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"error":"로그인이 필요합니다"}`))
return
}
redirectURL := "/login?next=" + r.URL.RequestURI()
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
// 로그인 상태를 컨텍스트에 저장
ctx := context.WithValue(r.Context(), CtxLoggedIn, true)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}

37
middleware/middleware.go Normal file
View File

@@ -0,0 +1,37 @@
package middleware
import (
"log"
"net/http"
"time"
)
// Logger HTTP 요청 로깅 미들웨어
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.RequestURI, time.Since(start))
})
}
// Recovery 패닉 발생 시 500 응답으로 복구하는 미들웨어
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("패닉 복구: %v", err)
http.Error(w, "내부 서버 오류가 발생했습니다.", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// Chain 여러 미들웨어를 순서대로 체인
func Chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}