242 lines
7.0 KiB
Go
242 lines
7.0 KiB
Go
package router
|
|
|
|
import (
|
|
"html/template"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"homework-manager/internal/config"
|
|
"homework-manager/internal/handler"
|
|
"homework-manager/internal/middleware"
|
|
"homework-manager/internal/service"
|
|
|
|
"github.com/gin-contrib/sessions"
|
|
"github.com/gin-contrib/sessions/cookie"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func getFuncMap() template.FuncMap {
|
|
return template.FuncMap{
|
|
"formatDate": func(t time.Time) string {
|
|
return t.Format("2006/01/02")
|
|
},
|
|
"formatDateTime": func(t time.Time) string {
|
|
return t.Format("2006/01/02 15:04")
|
|
},
|
|
"formatDateInput": func(t time.Time) string {
|
|
return t.Format("2006-01-02T15:04")
|
|
},
|
|
"isOverdue": func(t time.Time, completed bool) bool {
|
|
return !completed && time.Now().After(t)
|
|
},
|
|
"daysUntil": func(t time.Time) int {
|
|
return int(time.Until(t).Hours() / 24)
|
|
},
|
|
}
|
|
}
|
|
|
|
func loadTemplates() (*template.Template, error) {
|
|
tmpl := template.New("").Funcs(getFuncMap())
|
|
|
|
baseContent, err := os.ReadFile("web/templates/layouts/base.html")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
templateDirs := []struct {
|
|
pattern string
|
|
prefix string
|
|
}{
|
|
{"web/templates/auth/*.html", ""},
|
|
{"web/templates/pages/*.html", ""},
|
|
{"web/templates/assignments/*.html", "assignments/"},
|
|
{"web/templates/admin/*.html", "admin/"},
|
|
}
|
|
|
|
for _, dir := range templateDirs {
|
|
files, err := filepath.Glob(dir.pattern)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, file := range files {
|
|
name := dir.prefix + filepath.Base(file)
|
|
content, err := os.ReadFile(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reDefine := regexp.MustCompile(`{{\s*define\s+"([^"]+)"\s*}}`)
|
|
reTemplate := regexp.MustCompile(`{{\s*template\s+"([^"]+)"\s*([^}]*)\s*}}`)
|
|
|
|
uniqueBase := reDefine.ReplaceAllStringFunc(string(baseContent), func(m string) string {
|
|
match := reDefine.FindStringSubmatch(m)
|
|
blockName := match[1]
|
|
if blockName == "head" || blockName == "scripts" || blockName == "content" || blockName == "base" {
|
|
return strings.Replace(m, blockName, name+"_"+blockName, 1)
|
|
}
|
|
return m
|
|
})
|
|
|
|
uniqueBase = reTemplate.ReplaceAllStringFunc(uniqueBase, func(m string) string {
|
|
match := reTemplate.FindStringSubmatch(m)
|
|
blockName := match[1]
|
|
if blockName == "head" || blockName == "scripts" || blockName == "content" || blockName == "base" {
|
|
return strings.Replace(m, blockName, name+"_"+blockName, 1)
|
|
}
|
|
return m
|
|
})
|
|
uniqueContent := reDefine.ReplaceAllStringFunc(string(content), func(m string) string {
|
|
match := reDefine.FindStringSubmatch(m)
|
|
blockName := match[1]
|
|
if blockName == "head" || blockName == "scripts" || blockName == "content" {
|
|
return strings.Replace(m, blockName, name+"_"+blockName, 1)
|
|
}
|
|
return m
|
|
})
|
|
|
|
uniqueContent = reTemplate.ReplaceAllStringFunc(uniqueContent, func(m string) string {
|
|
match := reTemplate.FindStringSubmatch(m)
|
|
blockName := match[1]
|
|
if blockName == "base" {
|
|
return strings.Replace(m, blockName, name+"_"+blockName, 1)
|
|
}
|
|
return m
|
|
})
|
|
|
|
combined := uniqueBase + "\n" + uniqueContent
|
|
_, err = tmpl.New(name).Parse(combined)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return tmpl, nil
|
|
}
|
|
|
|
func Setup(cfg *config.Config) *gin.Engine {
|
|
if !cfg.Debug {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
}
|
|
|
|
r := gin.Default()
|
|
|
|
if len(cfg.TrustedProxies) > 0 {
|
|
r.SetTrustedProxies(cfg.TrustedProxies)
|
|
}
|
|
tmpl, err := loadTemplates()
|
|
if err != nil {
|
|
panic("Failed to load templates: " + err.Error())
|
|
}
|
|
r.SetHTMLTemplate(tmpl)
|
|
|
|
r.Static("/static", "web/static")
|
|
|
|
store := cookie.NewStore([]byte(cfg.SessionSecret))
|
|
store.Options(sessions.Options{
|
|
Path: "/",
|
|
MaxAge: 86400 * 7, // 7 days
|
|
HttpOnly: true,
|
|
Secure: cfg.HTTPS,
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
r.Use(sessions.Sessions("session", store))
|
|
r.Use(middleware.RequestTimer())
|
|
|
|
securityConfig := middleware.SecurityConfig{
|
|
HTTPS: cfg.HTTPS,
|
|
}
|
|
r.Use(middleware.SecurityHeaders(securityConfig))
|
|
r.Use(middleware.ForceHTTPS(securityConfig))
|
|
|
|
r.Use(middleware.RateLimit(middleware.RateLimitConfig{
|
|
Enabled: cfg.RateLimitEnabled,
|
|
Requests: cfg.RateLimitRequests,
|
|
Window: cfg.RateLimitWindow,
|
|
}))
|
|
|
|
csrfMiddleware := middleware.CSRF(middleware.CSRFConfig{
|
|
Secret: cfg.CSRFSecret,
|
|
})
|
|
|
|
authService := service.NewAuthService()
|
|
apiKeyService := service.NewAPIKeyService()
|
|
|
|
authHandler := handler.NewAuthHandler()
|
|
assignmentHandler := handler.NewAssignmentHandler()
|
|
adminHandler := handler.NewAdminHandler()
|
|
profileHandler := handler.NewProfileHandler()
|
|
apiHandler := handler.NewAPIHandler()
|
|
|
|
guest := r.Group("/")
|
|
guest.Use(middleware.GuestOnly())
|
|
guest.Use(csrfMiddleware)
|
|
{
|
|
guest.GET("/login", authHandler.ShowLogin)
|
|
guest.POST("/login", authHandler.Login)
|
|
if cfg.AllowRegistration {
|
|
guest.GET("/register", authHandler.ShowRegister)
|
|
guest.POST("/register", authHandler.Register)
|
|
} else {
|
|
guest.GET("/register", func(c *gin.Context) {
|
|
c.HTML(http.StatusForbidden, "error.html", gin.H{
|
|
"title": "登録無効",
|
|
"message": "新規登録は現在受け付けておりません。",
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
auth := r.Group("/")
|
|
auth.Use(middleware.AuthRequired(authService))
|
|
auth.Use(csrfMiddleware)
|
|
{
|
|
auth.GET("/", assignmentHandler.Dashboard)
|
|
auth.POST("/logout", authHandler.Logout)
|
|
|
|
auth.GET("/assignments", assignmentHandler.Index)
|
|
auth.GET("/assignments/new", assignmentHandler.New)
|
|
auth.POST("/assignments", assignmentHandler.Create)
|
|
auth.GET("/assignments/:id/edit", assignmentHandler.Edit)
|
|
auth.POST("/assignments/:id", assignmentHandler.Update)
|
|
auth.POST("/assignments/:id/toggle", assignmentHandler.Toggle)
|
|
auth.POST("/assignments/:id/delete", assignmentHandler.Delete)
|
|
|
|
auth.GET("/profile", profileHandler.Show)
|
|
auth.POST("/profile", profileHandler.Update)
|
|
auth.POST("/profile/password", profileHandler.ChangePassword)
|
|
admin := auth.Group("/admin")
|
|
admin.Use(middleware.AdminRequired())
|
|
{
|
|
admin.GET("/users", adminHandler.Index)
|
|
admin.POST("/users/:id/delete", adminHandler.DeleteUser)
|
|
admin.POST("/users/:id/role", adminHandler.ChangeRole)
|
|
|
|
admin.GET("/api-keys", adminHandler.APIKeys)
|
|
admin.POST("/api-keys", adminHandler.CreateAPIKey)
|
|
admin.POST("/api-keys/:id/delete", adminHandler.DeleteAPIKey)
|
|
}
|
|
}
|
|
|
|
api := r.Group("/api/v1")
|
|
api.Use(middleware.APIKeyAuth(apiKeyService))
|
|
{
|
|
api.GET("/assignments", apiHandler.ListAssignments)
|
|
api.GET("/assignments/pending", apiHandler.ListPendingAssignments)
|
|
api.GET("/assignments/completed", apiHandler.ListCompletedAssignments)
|
|
api.GET("/assignments/overdue", apiHandler.ListOverdueAssignments)
|
|
api.GET("/assignments/due-today", apiHandler.ListDueTodayAssignments)
|
|
api.GET("/assignments/due-this-week", apiHandler.ListDueThisWeekAssignments)
|
|
api.GET("/assignments/:id", apiHandler.GetAssignment)
|
|
api.POST("/assignments", apiHandler.CreateAssignment)
|
|
api.PUT("/assignments/:id", apiHandler.UpdateAssignment)
|
|
api.DELETE("/assignments/:id", apiHandler.DeleteAssignment)
|
|
api.PATCH("/assignments/:id/toggle", apiHandler.ToggleAssignment)
|
|
}
|
|
|
|
return r
|
|
}
|