2023-04-06 10:36:11 +05:00
|
|
|
package views
|
|
|
|
|
|
|
|
import (
|
2023-04-09 10:41:21 +05:00
|
|
|
"errors"
|
2023-07-22 13:42:43 +05:00
|
|
|
"fmt"
|
2023-08-27 19:26:16 +05:00
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
2023-04-06 10:36:11 +05:00
|
|
|
"github.com/gin-gonic/gin"
|
2023-07-22 13:42:43 +05:00
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
|
|
"github.com/ordinary-dev/phoenix/config"
|
2023-07-22 15:24:01 +05:00
|
|
|
"github.com/ordinary-dev/phoenix/database"
|
2023-04-06 10:36:11 +05:00
|
|
|
"gorm.io/gorm"
|
|
|
|
)
|
|
|
|
|
2023-11-01 20:54:24 +05:00
|
|
|
const TOKEN_LIFETIME_IN_SECONDS = 60 * 60 * 24 * 30
|
|
|
|
|
2023-11-01 23:18:33 +05:00
|
|
|
func ShowRegistrationForm(cfg *config.Config, db *gorm.DB) gin.HandlerFunc {
|
2023-11-01 22:40:15 +05:00
|
|
|
return func(ctx *gin.Context) {
|
|
|
|
if database.CountAdmins(db) > 0 {
|
2023-11-01 23:18:33 +05:00
|
|
|
ShowError(ctx, cfg, errors.New("At least 1 user already exists"))
|
2023-11-01 22:40:15 +05:00
|
|
|
return
|
|
|
|
}
|
2023-07-22 21:39:43 +05:00
|
|
|
|
2023-11-01 23:18:33 +05:00
|
|
|
Render(ctx, cfg, http.StatusOK, "auth.html.tmpl", gin.H{
|
2023-11-01 22:40:15 +05:00
|
|
|
"title": "Create an account",
|
|
|
|
"description": "To prevent other people from seeing your links, create an account.",
|
|
|
|
"button": "Create",
|
|
|
|
"formAction": "/api/users",
|
|
|
|
})
|
|
|
|
}
|
2023-04-06 10:36:11 +05:00
|
|
|
}
|
|
|
|
|
2023-11-01 23:18:33 +05:00
|
|
|
func ShowLoginForm(cfg *config.Config) gin.HandlerFunc {
|
|
|
|
return func(ctx *gin.Context) {
|
|
|
|
Render(ctx, cfg, http.StatusOK, "auth.html.tmpl", gin.H{
|
|
|
|
"title": "Sign in",
|
|
|
|
"description": "Authorization is required to view this page.",
|
|
|
|
"button": "Sign in",
|
|
|
|
"formAction": "/api/users/signin",
|
|
|
|
})
|
|
|
|
}
|
2023-04-06 10:36:11 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Requires the user to log in before viewing the page.
|
2023-04-09 10:41:21 +05:00
|
|
|
// Returns error if the user is not authorized.
|
|
|
|
// If `nil` is returned instead of an error, it is safe to display protected content.
|
2023-07-22 13:42:43 +05:00
|
|
|
func RequireAuth(c *gin.Context, cfg *config.Config) (*jwt.RegisteredClaims, error) {
|
|
|
|
tokenValue, err := c.Cookie("phoenix-token")
|
2023-04-06 10:36:11 +05:00
|
|
|
|
2023-07-22 13:42:43 +05:00
|
|
|
// Anonymous visitor
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2023-04-06 10:36:11 +05:00
|
|
|
}
|
|
|
|
|
2023-07-22 13:42:43 +05:00
|
|
|
// Check token
|
|
|
|
token, err := jwt.ParseWithClaims(tokenValue, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
|
|
|
|
// Validate the alg
|
|
|
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
|
|
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
|
|
|
}
|
2023-04-06 10:36:11 +05:00
|
|
|
|
2023-07-22 13:42:43 +05:00
|
|
|
return []byte(cfg.SecretKey), nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
claims, ok := token.Claims.(*jwt.RegisteredClaims)
|
|
|
|
if !ok || !token.Valid {
|
|
|
|
return nil, errors.New("Token is invalid")
|
|
|
|
}
|
|
|
|
|
|
|
|
return claims, nil
|
|
|
|
}
|
|
|
|
|
2023-11-01 22:40:15 +05:00
|
|
|
func AuthMiddleware(db *gorm.DB, cfg *config.Config) gin.HandlerFunc {
|
|
|
|
return func(ctx *gin.Context) {
|
|
|
|
claims, err := RequireAuth(ctx, cfg)
|
|
|
|
if err != nil {
|
|
|
|
if cfg.HeaderAuth && ctx.Request.Header.Get("Remote-User") != "" {
|
|
|
|
// Generate access token.
|
|
|
|
token, err := GetJWTToken(cfg)
|
|
|
|
if err != nil {
|
2023-11-01 23:18:33 +05:00
|
|
|
ShowError(ctx, cfg, err)
|
2023-11-01 22:40:15 +05:00
|
|
|
return
|
|
|
|
}
|
|
|
|
SetTokenCookie(ctx, token, cfg)
|
2023-08-27 19:26:16 +05:00
|
|
|
return
|
|
|
|
}
|
2023-08-27 19:44:35 +05:00
|
|
|
|
2023-11-01 22:40:15 +05:00
|
|
|
if database.CountAdmins(db) < 1 {
|
|
|
|
ctx.Redirect(http.StatusFound, "/registration")
|
|
|
|
} else {
|
|
|
|
ctx.Redirect(http.StatusFound, "/signin")
|
|
|
|
}
|
|
|
|
ctx.Abort()
|
|
|
|
return
|
2023-08-27 19:44:35 +05:00
|
|
|
}
|
2023-07-22 13:42:43 +05:00
|
|
|
|
2023-11-01 22:40:15 +05:00
|
|
|
// Create a new token if the old one is about to expire
|
|
|
|
if time.Now().Add(time.Second * (TOKEN_LIFETIME_IN_SECONDS / 2)).After(claims.ExpiresAt.Time) {
|
|
|
|
newToken, err := GetJWTToken(cfg)
|
|
|
|
if err != nil {
|
2023-11-01 23:18:33 +05:00
|
|
|
ShowError(ctx, cfg, err)
|
2023-11-01 22:40:15 +05:00
|
|
|
return
|
|
|
|
}
|
|
|
|
SetTokenCookie(ctx, newToken, cfg)
|
2023-07-22 13:42:43 +05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func GetJWTToken(cfg *config.Config) (string, error) {
|
|
|
|
claims := jwt.RegisteredClaims{
|
2023-11-01 20:54:24 +05:00
|
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * TOKEN_LIFETIME_IN_SECONDS)),
|
2023-07-22 13:42:43 +05:00
|
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
|
|
return token.SignedString([]byte(cfg.SecretKey))
|
|
|
|
}
|
|
|
|
|
2023-11-01 22:40:15 +05:00
|
|
|
func CreateUser(db *gorm.DB, cfg *config.Config) gin.HandlerFunc {
|
|
|
|
return func(ctx *gin.Context) {
|
|
|
|
if database.CountAdmins(db) > 0 {
|
2023-11-01 23:18:33 +05:00
|
|
|
ShowError(ctx, cfg, errors.New("At least 1 user already exists"))
|
2023-11-01 22:40:15 +05:00
|
|
|
return
|
|
|
|
}
|
2023-07-22 21:39:43 +05:00
|
|
|
|
2023-11-01 22:40:15 +05:00
|
|
|
// Try to create a user.
|
|
|
|
username := ctx.PostForm("username")
|
|
|
|
password := ctx.PostForm("password")
|
|
|
|
_, err := database.CreateAdmin(db, username, password)
|
|
|
|
if err != nil {
|
2023-11-01 23:18:33 +05:00
|
|
|
ShowError(ctx, cfg, err)
|
2023-11-01 22:40:15 +05:00
|
|
|
return
|
|
|
|
}
|
2023-04-06 10:36:11 +05:00
|
|
|
|
2023-11-01 22:40:15 +05:00
|
|
|
// Generate access token.
|
|
|
|
token, err := GetJWTToken(cfg)
|
|
|
|
if err != nil {
|
2023-11-01 23:18:33 +05:00
|
|
|
ShowError(ctx, cfg, err)
|
2023-11-01 22:40:15 +05:00
|
|
|
return
|
|
|
|
}
|
|
|
|
SetTokenCookie(ctx, token, cfg)
|
2023-07-22 13:42:43 +05:00
|
|
|
|
2023-11-01 22:40:15 +05:00
|
|
|
// Redirect to homepage.
|
|
|
|
ctx.Redirect(http.StatusFound, "/")
|
|
|
|
}
|
2023-07-22 13:42:43 +05:00
|
|
|
}
|
|
|
|
|
2023-11-01 22:40:15 +05:00
|
|
|
func AuthorizeUser(db *gorm.DB, cfg *config.Config) gin.HandlerFunc {
|
|
|
|
return func(ctx *gin.Context) {
|
|
|
|
// Check credentials.
|
|
|
|
username := ctx.PostForm("username")
|
|
|
|
password := ctx.PostForm("password")
|
|
|
|
_, err := database.AuthorizeAdmin(db, username, password)
|
|
|
|
if err != nil {
|
2023-11-01 23:18:33 +05:00
|
|
|
ShowError(ctx, cfg, err)
|
2023-11-01 22:40:15 +05:00
|
|
|
return
|
|
|
|
}
|
2023-07-22 13:42:43 +05:00
|
|
|
|
2023-11-01 22:40:15 +05:00
|
|
|
// Generate an access token.
|
|
|
|
token, err := GetJWTToken(cfg)
|
|
|
|
if err != nil {
|
2023-11-01 23:18:33 +05:00
|
|
|
ShowError(ctx, cfg, err)
|
2023-11-01 22:40:15 +05:00
|
|
|
return
|
|
|
|
}
|
|
|
|
SetTokenCookie(ctx, token, cfg)
|
2023-07-22 13:42:43 +05:00
|
|
|
|
2023-11-01 22:40:15 +05:00
|
|
|
// Redirect to homepage.
|
|
|
|
ctx.Redirect(http.StatusFound, "/")
|
|
|
|
}
|
2023-07-22 13:42:43 +05:00
|
|
|
}
|
2023-04-09 10:41:21 +05:00
|
|
|
|
2023-11-01 21:02:08 +05:00
|
|
|
// Save token in cookies
|
|
|
|
func SetTokenCookie(c *gin.Context, token string, cfg *config.Config) {
|
|
|
|
c.SetCookie("phoenix-token", token, TOKEN_LIFETIME_IN_SECONDS, "/", "", cfg.SecureCookie, true)
|
2023-04-06 10:36:11 +05:00
|
|
|
}
|