2023-09-18 23:57:18 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/md5"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
2023-10-03 20:47:38 +00:00
|
|
|
"time"
|
2023-09-18 23:57:18 +00:00
|
|
|
|
|
|
|
argon2 "github.com/alexedwards/argon2id"
|
|
|
|
"github.com/gin-contrib/sessions"
|
|
|
|
"github.com/gin-gonic/gin"
|
2023-10-03 20:47:38 +00:00
|
|
|
log "github.com/sirupsen/logrus"
|
2023-09-18 23:57:18 +00:00
|
|
|
"reichard.io/bbank/database"
|
|
|
|
)
|
|
|
|
|
2024-01-10 02:08:40 +00:00
|
|
|
// Authorization Data
|
|
|
|
type authData struct {
|
|
|
|
UserName string
|
|
|
|
IsAdmin bool
|
|
|
|
}
|
|
|
|
|
2023-09-26 23:14:33 +00:00
|
|
|
// KOSync API Auth Headers
|
2023-10-05 23:56:19 +00:00
|
|
|
type authKOHeader struct {
|
2023-09-18 23:57:18 +00:00
|
|
|
AuthUser string `header:"x-auth-user"`
|
|
|
|
AuthKey string `header:"x-auth-key"`
|
|
|
|
}
|
|
|
|
|
2023-10-05 23:56:19 +00:00
|
|
|
// OPDS Auth Headers
|
|
|
|
type authOPDSHeader struct {
|
|
|
|
Authorization string `header:"authorization"`
|
|
|
|
}
|
|
|
|
|
2024-01-10 02:08:40 +00:00
|
|
|
func (api *API) authorizeCredentials(username string, password string) (auth *authData) {
|
2023-09-18 23:57:18 +00:00
|
|
|
user, err := api.DB.Queries.GetUser(api.DB.Ctx, username)
|
|
|
|
if err != nil {
|
2024-01-10 02:08:40 +00:00
|
|
|
return
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
|
|
|
|
2023-09-27 22:58:47 +00:00
|
|
|
if match, err := argon2.ComparePasswordAndHash(password, *user.Pass); err != nil || match != true {
|
2024-01-10 02:08:40 +00:00
|
|
|
return
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
|
|
|
|
2024-01-10 02:08:40 +00:00
|
|
|
return &authData{
|
|
|
|
UserName: user.ID,
|
|
|
|
IsAdmin: user.Admin,
|
|
|
|
}
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
|
|
|
|
2023-10-05 23:56:19 +00:00
|
|
|
func (api *API) authKOMiddleware(c *gin.Context) {
|
2023-09-18 23:57:18 +00:00
|
|
|
session := sessions.Default(c)
|
|
|
|
|
2023-10-03 20:47:38 +00:00
|
|
|
// Check Session First
|
2024-01-10 02:08:40 +00:00
|
|
|
if auth, ok := getSession(session); ok == true {
|
|
|
|
c.Set("Authorization", auth)
|
2023-09-21 00:35:01 +00:00
|
|
|
c.Header("Cache-Control", "private")
|
2023-09-18 23:57:18 +00:00
|
|
|
c.Next()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-10-03 20:47:38 +00:00
|
|
|
// Session Failed -> Check Headers (Allowed on API for KOSync Compatibility)
|
|
|
|
|
2023-10-05 23:56:19 +00:00
|
|
|
var rHeader authKOHeader
|
2023-09-18 23:57:18 +00:00
|
|
|
if err := c.ShouldBindHeader(&rHeader); err != nil {
|
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Incorrect Headers"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if rHeader.AuthUser == "" || rHeader.AuthKey == "" {
|
|
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization Headers"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-10 02:08:40 +00:00
|
|
|
authData := api.authorizeCredentials(rHeader.AuthUser, rHeader.AuthKey)
|
|
|
|
if authData == nil {
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-10 02:08:40 +00:00
|
|
|
if err := setSession(session, *authData); err != nil {
|
2023-10-03 20:47:38 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
|
|
|
return
|
|
|
|
}
|
2023-09-18 23:57:18 +00:00
|
|
|
|
2024-01-10 02:08:40 +00:00
|
|
|
c.Set("Authorization", *authData)
|
2023-10-03 20:47:38 +00:00
|
|
|
c.Header("Cache-Control", "private")
|
2023-09-18 23:57:18 +00:00
|
|
|
c.Next()
|
|
|
|
}
|
|
|
|
|
2023-10-05 23:56:19 +00:00
|
|
|
func (api *API) authOPDSMiddleware(c *gin.Context) {
|
|
|
|
c.Header("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
|
|
|
|
|
|
|
user, rawPassword, hasAuth := c.Request.BasicAuth()
|
|
|
|
|
|
|
|
// Validate Auth Fields
|
|
|
|
if hasAuth != true || user == "" || rawPassword == "" {
|
|
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization Headers"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate Auth
|
|
|
|
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
2024-01-10 02:08:40 +00:00
|
|
|
authData := api.authorizeCredentials(user, password)
|
|
|
|
if authData == nil {
|
2023-10-05 23:56:19 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-10 02:08:40 +00:00
|
|
|
c.Set("Authorization", *authData)
|
2023-10-05 23:56:19 +00:00
|
|
|
c.Header("Cache-Control", "private")
|
|
|
|
c.Next()
|
|
|
|
}
|
|
|
|
|
2023-09-18 23:57:18 +00:00
|
|
|
func (api *API) authWebAppMiddleware(c *gin.Context) {
|
|
|
|
session := sessions.Default(c)
|
|
|
|
|
2023-10-03 20:47:38 +00:00
|
|
|
// Check Session
|
2024-01-10 02:08:40 +00:00
|
|
|
if auth, ok := getSession(session); ok == true {
|
|
|
|
c.Set("Authorization", auth)
|
2023-09-21 00:35:01 +00:00
|
|
|
c.Header("Cache-Control", "private")
|
2023-09-18 23:57:18 +00:00
|
|
|
c.Next()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.Redirect(http.StatusFound, "/login")
|
|
|
|
c.Abort()
|
2023-10-25 23:52:01 +00:00
|
|
|
return
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
|
|
|
|
2024-01-10 02:08:40 +00:00
|
|
|
func (api *API) authAdminWebAppMiddleware(c *gin.Context) {
|
|
|
|
if data, _ := c.Get("Authorization"); data != nil {
|
|
|
|
auth := data.(authData)
|
|
|
|
if auth.IsAdmin == true {
|
|
|
|
c.Next()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
errorPage(c, http.StatusUnauthorized, "Admin Permissions Required")
|
|
|
|
c.Abort()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-01 04:12:46 +00:00
|
|
|
func (api *API) appAuthFormLogin(c *gin.Context) {
|
|
|
|
templateVars := api.getBaseTemplateVars("login", c)
|
|
|
|
|
2023-09-18 23:57:18 +00:00
|
|
|
username := strings.TrimSpace(c.PostForm("username"))
|
|
|
|
rawPassword := strings.TrimSpace(c.PostForm("password"))
|
|
|
|
|
|
|
|
if username == "" || rawPassword == "" {
|
2024-01-01 04:12:46 +00:00
|
|
|
templateVars["Error"] = "Invalid Credentials"
|
|
|
|
c.HTML(http.StatusUnauthorized, "page/login", templateVars)
|
2023-09-18 23:57:18 +00:00
|
|
|
return
|
|
|
|
}
|
2023-09-26 23:14:33 +00:00
|
|
|
|
|
|
|
// MD5 - KOSync Compatiblity
|
2023-09-18 23:57:18 +00:00
|
|
|
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
2024-01-10 02:08:40 +00:00
|
|
|
authData := api.authorizeCredentials(username, password)
|
|
|
|
if authData == nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
templateVars["Error"] = "Invalid Credentials"
|
|
|
|
c.HTML(http.StatusUnauthorized, "page/login", templateVars)
|
2023-09-18 23:57:18 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-10-03 20:47:38 +00:00
|
|
|
// Set Session
|
2023-09-18 23:57:18 +00:00
|
|
|
session := sessions.Default(c)
|
2024-01-10 02:08:40 +00:00
|
|
|
if err := setSession(session, *authData); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
templateVars["Error"] = "Invalid Credentials"
|
|
|
|
c.HTML(http.StatusUnauthorized, "page/login", templateVars)
|
2023-10-03 20:47:38 +00:00
|
|
|
return
|
|
|
|
}
|
2023-09-18 23:57:18 +00:00
|
|
|
|
2023-10-03 20:47:38 +00:00
|
|
|
c.Header("Cache-Control", "private")
|
2023-09-18 23:57:18 +00:00
|
|
|
c.Redirect(http.StatusFound, "/")
|
|
|
|
}
|
|
|
|
|
2024-01-01 04:12:46 +00:00
|
|
|
func (api *API) appAuthFormRegister(c *gin.Context) {
|
2023-09-19 23:29:55 +00:00
|
|
|
if !api.Config.RegistrationEnabled {
|
2023-10-25 23:52:01 +00:00
|
|
|
errorPage(c, http.StatusUnauthorized, "Nice try. Registration is disabled.")
|
|
|
|
return
|
2023-09-19 23:29:55 +00:00
|
|
|
}
|
|
|
|
|
2024-01-01 04:12:46 +00:00
|
|
|
templateVars := api.getBaseTemplateVars("login", c)
|
|
|
|
templateVars["Register"] = true
|
|
|
|
|
2023-09-18 23:57:18 +00:00
|
|
|
username := strings.TrimSpace(c.PostForm("username"))
|
|
|
|
rawPassword := strings.TrimSpace(c.PostForm("password"))
|
|
|
|
|
|
|
|
if username == "" || rawPassword == "" {
|
2024-01-01 04:12:46 +00:00
|
|
|
templateVars["Error"] = "Invalid User or Password"
|
|
|
|
c.HTML(http.StatusBadRequest, "page/login", templateVars)
|
2023-09-18 23:57:18 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
|
|
|
|
|
|
|
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
templateVars["Error"] = "Registration Disabled or User Already Exists"
|
|
|
|
c.HTML(http.StatusBadRequest, "page/login", templateVars)
|
2023-09-18 23:57:18 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
rows, err := api.DB.Queries.CreateUser(api.DB.Ctx, database.CreateUserParams{
|
|
|
|
ID: username,
|
2023-09-27 22:58:47 +00:00
|
|
|
Pass: &hashedPassword,
|
2023-09-18 23:57:18 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
// SQL Error
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[appAuthFormRegister] CreateUser DB Error:", err)
|
|
|
|
templateVars["Error"] = "Registration Disabled or User Already Exists"
|
|
|
|
c.HTML(http.StatusBadRequest, "page/login", templateVars)
|
2023-09-18 23:57:18 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// User Already Exists
|
|
|
|
if rows == 0 {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Warn("[appAuthFormRegister] User Already Exists:", username)
|
|
|
|
templateVars["Error"] = "Registration Disabled or User Already Exists"
|
|
|
|
c.HTML(http.StatusBadRequest, "page/login", templateVars)
|
2023-09-18 23:57:18 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-10 02:08:40 +00:00
|
|
|
// Get User
|
|
|
|
user, err := api.DB.Queries.GetUser(api.DB.Ctx, username)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("[appAuthFormRegister] GetUser DB Error:", err)
|
|
|
|
templateVars["Error"] = "Registration Disabled or User Already Exists"
|
|
|
|
c.HTML(http.StatusBadRequest, "page/login", templateVars)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-10-03 20:47:38 +00:00
|
|
|
// Set Session
|
2024-01-10 02:08:40 +00:00
|
|
|
auth := authData{
|
|
|
|
UserName: user.ID,
|
|
|
|
IsAdmin: user.Admin,
|
|
|
|
}
|
2023-09-18 23:57:18 +00:00
|
|
|
session := sessions.Default(c)
|
2024-01-10 02:08:40 +00:00
|
|
|
if err := setSession(session, auth); err != nil {
|
2023-10-25 23:52:01 +00:00
|
|
|
errorPage(c, http.StatusUnauthorized, "Unauthorized.")
|
2023-10-03 20:47:38 +00:00
|
|
|
return
|
|
|
|
}
|
2023-09-18 23:57:18 +00:00
|
|
|
|
2023-10-03 20:47:38 +00:00
|
|
|
c.Header("Cache-Control", "private")
|
2023-09-18 23:57:18 +00:00
|
|
|
c.Redirect(http.StatusFound, "/")
|
|
|
|
}
|
2023-09-26 23:14:33 +00:00
|
|
|
|
2024-01-01 04:12:46 +00:00
|
|
|
func (api *API) appAuthLogout(c *gin.Context) {
|
2023-09-26 23:14:33 +00:00
|
|
|
session := sessions.Default(c)
|
|
|
|
session.Clear()
|
|
|
|
session.Save()
|
|
|
|
c.Redirect(http.StatusFound, "/login")
|
|
|
|
}
|
2023-10-03 20:47:38 +00:00
|
|
|
|
2024-01-10 02:08:40 +00:00
|
|
|
func getSession(session sessions.Session) (auth authData, ok bool) {
|
2023-10-03 20:47:38 +00:00
|
|
|
// Check Session
|
|
|
|
authorizedUser := session.Get("authorizedUser")
|
2024-01-10 02:08:40 +00:00
|
|
|
isAdmin := session.Get("isAdmin")
|
|
|
|
expiresAt := session.Get("expiresAt")
|
|
|
|
if authorizedUser == nil || isAdmin == nil || expiresAt == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create Auth Object
|
|
|
|
auth = authData{
|
|
|
|
UserName: authorizedUser.(string),
|
|
|
|
IsAdmin: isAdmin.(bool),
|
2023-10-03 20:47:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Refresh
|
2024-01-10 02:08:40 +00:00
|
|
|
if expiresAt.(int64)-time.Now().Unix() < 60*60*24 {
|
2023-10-03 20:47:38 +00:00
|
|
|
log.Info("[getSession] Refreshing Session")
|
2024-01-10 02:08:40 +00:00
|
|
|
setSession(session, auth)
|
2023-10-03 20:47:38 +00:00
|
|
|
}
|
|
|
|
|
2024-01-10 02:08:40 +00:00
|
|
|
// Authorized
|
|
|
|
return auth, true
|
2023-10-03 20:47:38 +00:00
|
|
|
}
|
|
|
|
|
2024-01-10 02:08:40 +00:00
|
|
|
func setSession(session sessions.Session, auth authData) error {
|
2023-10-03 20:47:38 +00:00
|
|
|
// Set Session Cookie
|
2024-01-10 02:08:40 +00:00
|
|
|
session.Set("authorizedUser", auth.UserName)
|
|
|
|
session.Set("isAdmin", auth.IsAdmin)
|
2023-10-03 20:47:38 +00:00
|
|
|
session.Set("expiresAt", time.Now().Unix()+(60*60*24*7))
|
|
|
|
return session.Save()
|
|
|
|
}
|