230 lines
5.6 KiB
Go
230 lines
5.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"homework-manager/internal/config"
|
|
"homework-manager/internal/middleware"
|
|
"homework-manager/internal/service"
|
|
|
|
"github.com/gin-contrib/sessions"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const twoFAPendingKey = "2fa_pending_user_id"
|
|
|
|
type AuthHandler struct {
|
|
authService *service.AuthService
|
|
totpService *service.TOTPService
|
|
captchaService *service.CaptchaService
|
|
captchaCfg config.CaptchaConfig
|
|
}
|
|
|
|
func NewAuthHandler(captchaCfg config.CaptchaConfig) *AuthHandler {
|
|
captchaSvc := service.NewCaptchaService(captchaCfg.Type, captchaCfg.TurnstileSecretKey)
|
|
return &AuthHandler{
|
|
authService: service.NewAuthService(),
|
|
totpService: service.NewTOTPService(),
|
|
captchaService: captchaSvc,
|
|
captchaCfg: captchaCfg,
|
|
}
|
|
}
|
|
|
|
func (h *AuthHandler) captchaData() gin.H {
|
|
data := gin.H{
|
|
"captchaEnabled": h.captchaCfg.Enabled,
|
|
"captchaType": h.captchaCfg.Type,
|
|
}
|
|
if h.captchaCfg.Enabled && h.captchaCfg.Type == "turnstile" {
|
|
data["turnstileSiteKey"] = h.captchaCfg.TurnstileSiteKey
|
|
}
|
|
if h.captchaCfg.Enabled && h.captchaCfg.Type == "image" {
|
|
data["captchaID"] = h.captchaService.NewImageCaptcha()
|
|
}
|
|
return data
|
|
}
|
|
|
|
func (h *AuthHandler) verifyCaptcha(c *gin.Context) bool {
|
|
if !h.captchaCfg.Enabled {
|
|
return true
|
|
}
|
|
switch h.captchaCfg.Type {
|
|
case "turnstile":
|
|
token := c.PostForm("cf-turnstile-response")
|
|
ok, err := h.captchaService.VerifyTurnstile(token, c.ClientIP())
|
|
return err == nil && ok
|
|
case "image":
|
|
id := c.PostForm("captcha_id")
|
|
answer := c.PostForm("captcha_answer")
|
|
return h.captchaService.VerifyImageCaptcha(id, answer)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (h *AuthHandler) ShowLogin(c *gin.Context) {
|
|
data := gin.H{"title": "ログイン"}
|
|
for k, v := range h.captchaData() {
|
|
data[k] = v
|
|
}
|
|
RenderHTML(c, http.StatusOK, "login.html", data)
|
|
}
|
|
|
|
func (h *AuthHandler) Login(c *gin.Context) {
|
|
email := c.PostForm("email")
|
|
password := c.PostForm("password")
|
|
|
|
renderLoginError := func(msg string) {
|
|
data := gin.H{
|
|
"title": "ログイン",
|
|
"error": msg,
|
|
"email": email,
|
|
}
|
|
for k, v := range h.captchaData() {
|
|
data[k] = v
|
|
}
|
|
RenderHTML(c, http.StatusOK, "login.html", data)
|
|
}
|
|
|
|
if !h.verifyCaptcha(c) {
|
|
renderLoginError("CAPTCHAの検証に失敗しました。もう一度お試しください")
|
|
return
|
|
}
|
|
|
|
user, err := h.authService.Login(email, password)
|
|
if err != nil {
|
|
renderLoginError("メールアドレスまたはパスワードが正しくありません")
|
|
return
|
|
}
|
|
|
|
if user.TOTPEnabled {
|
|
session := sessions.Default(c)
|
|
session.Set(twoFAPendingKey, user.ID)
|
|
session.Save()
|
|
c.Redirect(http.StatusFound, "/login/2fa")
|
|
return
|
|
}
|
|
|
|
session := sessions.Default(c)
|
|
session.Set(middleware.UserIDKey, user.ID)
|
|
session.Set(middleware.UserRoleKey, user.Role)
|
|
session.Set(middleware.UserNameKey, user.Name)
|
|
session.Save()
|
|
|
|
c.Redirect(http.StatusFound, "/")
|
|
}
|
|
|
|
func (h *AuthHandler) ShowLogin2FA(c *gin.Context) {
|
|
session := sessions.Default(c)
|
|
if session.Get(twoFAPendingKey) == nil {
|
|
c.Redirect(http.StatusFound, "/login")
|
|
return
|
|
}
|
|
RenderHTML(c, http.StatusOK, "login_2fa.html", gin.H{
|
|
"title": "2段階認証",
|
|
})
|
|
}
|
|
|
|
func (h *AuthHandler) Login2FA(c *gin.Context) {
|
|
session := sessions.Default(c)
|
|
pendingID := session.Get(twoFAPendingKey)
|
|
if pendingID == nil {
|
|
c.Redirect(http.StatusFound, "/login")
|
|
return
|
|
}
|
|
|
|
userID := pendingID.(uint)
|
|
user, err := h.authService.GetUserByID(userID)
|
|
if err != nil {
|
|
session.Delete(twoFAPendingKey)
|
|
session.Save()
|
|
c.Redirect(http.StatusFound, "/login")
|
|
return
|
|
}
|
|
|
|
code := c.PostForm("totp_code")
|
|
if !h.totpService.Validate(user.TOTPSecret, code) {
|
|
RenderHTML(c, http.StatusOK, "login_2fa.html", gin.H{
|
|
"title": "2段階認証",
|
|
"error": "認証コードが正しくありません",
|
|
})
|
|
return
|
|
}
|
|
|
|
session.Delete(twoFAPendingKey)
|
|
session.Set(middleware.UserIDKey, user.ID)
|
|
session.Set(middleware.UserRoleKey, user.Role)
|
|
session.Set(middleware.UserNameKey, user.Name)
|
|
session.Save()
|
|
|
|
c.Redirect(http.StatusFound, "/")
|
|
}
|
|
|
|
func (h *AuthHandler) ShowRegister(c *gin.Context) {
|
|
data := gin.H{"title": "新規登録"}
|
|
for k, v := range h.captchaData() {
|
|
data[k] = v
|
|
}
|
|
RenderHTML(c, http.StatusOK, "register.html", data)
|
|
}
|
|
|
|
func (h *AuthHandler) Register(c *gin.Context) {
|
|
email := c.PostForm("email")
|
|
password := c.PostForm("password")
|
|
passwordConfirm := c.PostForm("password_confirm")
|
|
name := c.PostForm("name")
|
|
|
|
renderRegisterError := func(msg string) {
|
|
data := gin.H{
|
|
"title": "新規登録",
|
|
"error": msg,
|
|
"email": email,
|
|
"name": name,
|
|
}
|
|
for k, v := range h.captchaData() {
|
|
data[k] = v
|
|
}
|
|
RenderHTML(c, http.StatusOK, "register.html", data)
|
|
}
|
|
|
|
if !h.verifyCaptcha(c) {
|
|
renderRegisterError("CAPTCHAの検証に失敗しました。もう一度お試しください")
|
|
return
|
|
}
|
|
|
|
if password != passwordConfirm {
|
|
renderRegisterError("パスワードが一致しません")
|
|
return
|
|
}
|
|
|
|
if len(password) < 8 {
|
|
renderRegisterError("パスワードは8文字以上で入力してください")
|
|
return
|
|
}
|
|
|
|
user, err := h.authService.Register(email, password, name)
|
|
if err != nil {
|
|
errorMsg := "登録に失敗しました"
|
|
if err == service.ErrEmailAlreadyExists {
|
|
errorMsg = "このメールアドレスは既に使用されています"
|
|
}
|
|
renderRegisterError(errorMsg)
|
|
return
|
|
}
|
|
|
|
session := sessions.Default(c)
|
|
session.Set(middleware.UserIDKey, user.ID)
|
|
session.Set(middleware.UserRoleKey, user.Role)
|
|
session.Set(middleware.UserNameKey, user.Name)
|
|
session.Save()
|
|
|
|
c.Redirect(http.StatusFound, "/")
|
|
}
|
|
|
|
func (h *AuthHandler) Logout(c *gin.Context) {
|
|
session := sessions.Default(c)
|
|
session.Clear()
|
|
session.Save()
|
|
|
|
c.Redirect(http.StatusFound, "/login")
|
|
}
|