You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
synctv/server/middlewares/auth.go

377 lines
9.4 KiB
Go

package middlewares
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/sirupsen/logrus"
"github.com/synctv-org/synctv/internal/conf"
"github.com/synctv-org/synctv/internal/db"
dbModel "github.com/synctv-org/synctv/internal/model"
"github.com/synctv-org/synctv/internal/op"
"github.com/synctv-org/synctv/server/model"
"github.com/zijiren233/gencontainer/synccache"
"github.com/zijiren233/stream"
)
var (
ErrAuthFailed = errors.New("auth failed")
ErrAuthExpired = errors.New("auth expired")
)
type AuthClaims struct {
UserId string `json:"u"`
UserVersion uint32 `json:"uv"`
jwt.RegisteredClaims
}
type AuthRoomClaims struct {
AuthClaims
RoomId string `json:"r"`
RoomVersion uint32 `json:"rv"`
}
func authRoom(Authorization string) (*AuthRoomClaims, error) {
t, err := jwt.ParseWithClaims(strings.TrimPrefix(Authorization, `Bearer `), &AuthRoomClaims{}, func(token *jwt.Token) (any, error) {
return stream.StringToBytes(conf.Conf.Jwt.Secret), nil
})
if err != nil {
return nil, ErrAuthFailed
}
claims, ok := t.Claims.(*AuthRoomClaims)
if !ok || !t.Valid {
return nil, ErrAuthFailed
}
return claims, nil
}
func authUser(Authorization string) (*AuthClaims, error) {
t, err := jwt.ParseWithClaims(strings.TrimPrefix(Authorization, `Bearer `), &AuthClaims{}, func(token *jwt.Token) (any, error) {
return stream.StringToBytes(conf.Conf.Jwt.Secret), nil
})
if err != nil {
return nil, ErrAuthFailed
}
claims, ok := t.Claims.(*AuthClaims)
if !ok || !t.Valid {
return nil, ErrAuthFailed
}
return claims, nil
}
func AuthRoom(Authorization string) (*op.UserEntry, *op.RoomEntry, error) {
claims, err := authRoom(Authorization)
if err != nil {
return nil, nil, err
}
if len(claims.RoomId) != 32 {
return nil, nil, ErrAuthFailed
}
if len(claims.UserId) != 32 {
return nil, nil, ErrAuthFailed
}
userE, err := op.LoadOrInitUserByID(claims.UserId)
if err != nil {
return nil, nil, err
}
user := userE.Value()
if !user.CheckVersion(claims.UserVersion) {
return nil, nil, ErrAuthExpired
}
roomE, err := op.LoadOrInitRoomByID(claims.RoomId)
if err != nil {
return nil, nil, err
}
room := roomE.Value()
if !room.CheckVersion(claims.RoomVersion) {
return nil, nil, ErrAuthExpired
}
rus, err := room.LoadOrCreateMemberStatus(user.ID)
if err != nil {
return nil, nil, err
}
if !rus.IsActive() {
if rus.IsPending() {
return nil, nil, fmt.Errorf("user is pending, need admin to approve")
}
return nil, nil, fmt.Errorf("user is banned")
}
return userE, roomE, nil
}
func AuthUser(Authorization string) (*op.UserEntry, error) {
claims, err := authUser(Authorization)
if err != nil {
return nil, err
}
if len(claims.UserId) != 32 {
return nil, ErrAuthFailed
}
userE, err := op.LoadOrInitUserByID(claims.UserId)
if err != nil {
return nil, err
}
user := userE.Value()
if user.IsGuest() {
return nil, errors.New("user is guest, can not login")
}
if !user.CheckVersion(claims.UserVersion) {
return nil, ErrAuthExpired
}
return userE, nil
}
func NewAuthUserToken(user *op.User) (string, error) {
if user.IsBanned() {
return "", errors.New("user banned")
}
if user.IsPending() {
return "", errors.New("user is pending, need admin to approve")
}
if user.IsGuest() {
return "", errors.New("user is guest, can not login")
}
t, err := time.ParseDuration(conf.Conf.Jwt.Expire)
if err != nil {
return "", err
}
claims := &AuthClaims{
UserId: user.ID,
UserVersion: user.Version(),
RegisteredClaims: jwt.RegisteredClaims{
NotBefore: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(t)),
},
}
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(stream.StringToBytes(conf.Conf.Jwt.Secret))
}
func NewAuthRoomToken(user *op.User, room *op.Room) (string, error) {
if user.IsBanned() {
return "", errors.New("user banned")
}
if user.IsPending() {
return "", errors.New("user is pending, need admin to approve")
}
if room.IsBanned() {
return "", errors.New("room banned")
}
if room.IsPending() {
return "", errors.New("room is pending, need admin to approve")
}
member, err := room.LoadOrCreateRoomMember(user.ID)
if err != nil {
if errors.Is(err, db.ErrNotFound("")) {
return "", fmt.Errorf("this room was disabled join new user")
}
return "", fmt.Errorf("load room member failed: %w", err)
}
switch member.Status {
case dbModel.RoomMemberStatusBanned:
return "", fmt.Errorf("user is banned")
case dbModel.RoomMemberStatusPending:
return "", fmt.Errorf("user is pending, need admin to approve")
default:
if member.Status.IsNotActive() {
return "", fmt.Errorf("user is not active")
}
}
t, err := time.ParseDuration(conf.Conf.Jwt.Expire)
if err != nil {
return "", fmt.Errorf("parse jwt expire failed: %w", err)
}
claims := &AuthRoomClaims{
AuthClaims: AuthClaims{
UserId: user.ID,
UserVersion: user.Version(),
RegisteredClaims: jwt.RegisteredClaims{
NotBefore: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(t)),
},
},
RoomId: room.ID,
RoomVersion: room.Version(),
}
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(stream.StringToBytes(conf.Conf.Jwt.Secret))
}
func AuthUserMiddleware(ctx *gin.Context) {
token, err := GetAuthorizationTokenFromContext(ctx)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, model.NewApiErrorResp(err))
return
}
userE, err := AuthUser(token)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, model.NewApiErrorResp(err))
return
}
user := userE.Value()
if user.IsBanned() {
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user banned"))
return
}
if user.IsPending() {
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user is pending, need admin to approve"))
return
}
ctx.Set("user", userE)
log := ctx.MustGet("log").(*logrus.Entry)
if log.Data == nil {
log.Data = make(logrus.Fields, 3)
}
log.Data["uid"] = user.ID
log.Data["unm"] = user.Username
log.Data["uro"] = user.Role.String()
}
func AuthRoomMiddleware(ctx *gin.Context) {
token, err := GetAuthorizationTokenFromContext(ctx)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, model.NewApiErrorResp(err))
return
}
userE, roomE, err := AuthRoom(token)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, model.NewApiErrorResp(err))
return
}
user := userE.Value()
if user.IsBanned() {
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user banned"))
return
}
if user.IsPending() {
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user is pending, need admin to approve"))
return
}
room := roomE.Value()
if room.IsBanned() {
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("room banned"))
return
}
if room.IsPending() {
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("room is pending, need admin to approve"))
return
}
ctx.Set("user", userE)
ctx.Set("room", roomE)
log := ctx.MustGet("log").(*logrus.Entry)
if log.Data == nil {
log.Data = make(logrus.Fields, 5)
}
log.Data["rid"] = room.ID
log.Data["rnm"] = room.Name
log.Data["uid"] = user.ID
log.Data["unm"] = user.Username
log.Data["uro"] = user.Role.String()
}
func AuthRoomWithoutGuestMiddleware(ctx *gin.Context) {
AuthRoomMiddleware(ctx)
if ctx.IsAborted() {
return
}
user := ctx.MustGet("user").(*synccache.Entry[*op.User]).Value()
if user.IsGuest() {
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("guest is no permission"))
return
}
}
func AuthRoomAdminMiddleware(ctx *gin.Context) {
AuthRoomMiddleware(ctx)
if ctx.IsAborted() {
return
}
room := ctx.MustGet("room").(*synccache.Entry[*op.Room]).Value()
user := ctx.MustGet("user").(*synccache.Entry[*op.User]).Value()
if !user.IsRoomAdmin(room) {
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user has no permission"))
return
}
}
func AuthRoomCreatorMiddleware(ctx *gin.Context) {
AuthRoomMiddleware(ctx)
if ctx.IsAborted() {
return
}
room := ctx.MustGet("room").(*synccache.Entry[*op.Room]).Value()
user := ctx.MustGet("user").(*synccache.Entry[*op.User]).Value()
if room.CreatorID != user.ID {
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user is not creator"))
return
}
}
func AuthAdminMiddleware(ctx *gin.Context) {
AuthUserMiddleware(ctx)
if ctx.IsAborted() {
return
}
userE := ctx.MustGet("user").(*synccache.Entry[*op.User])
if !userE.Value().IsAdmin() {
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user is not admin"))
return
}
}
func AuthRootMiddleware(ctx *gin.Context) {
AuthUserMiddleware(ctx)
if ctx.IsAborted() {
return
}
userE := ctx.MustGet("user").(*synccache.Entry[*op.User])
if !userE.Value().IsRoot() {
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user is not root"))
return
}
}
func GetAuthorizationTokenFromContext(ctx *gin.Context) (string, error) {
Authorization := ctx.GetHeader("Authorization")
if Authorization != "" {
ctx.Set("token", Authorization)
return Authorization, nil
}
Authorization = ctx.Query("token")
if Authorization != "" {
ctx.Set("token", Authorization)
return Authorization, nil
}
return "", errors.New("token is empty")
}