package auth import ( "context" "errors" "fmt" "net/http" "time" "github.com/gin-gonic/gin" "github.com/synctv-org/synctv/internal/bootstrap" "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/internal/provider" "github.com/synctv-org/synctv/internal/provider/providers" "github.com/synctv-org/synctv/internal/settings" "github.com/synctv-org/synctv/server/middlewares" "github.com/synctv-org/synctv/server/model" "github.com/synctv-org/synctv/utils" ) // GET // /oauth2/login/:type func OAuth2(ctx *gin.Context) { pi, err := providers.GetProvider(provider.OAuth2Provider(ctx.Param("type"))) if err != nil { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } state := utils.RandString(16) states.Store(state, stateMeta{ OAuth2Req: model.OAuth2Req{ Redirect: ctx.Query("redirect"), }, }, time.Minute*5) RenderRedirect(ctx, pi.NewAuthURL(state)) } // POST func OAuth2Api(ctx *gin.Context) { pi, err := providers.GetProvider(provider.OAuth2Provider(ctx.Param("type"))) if err != nil { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) } meta := model.OAuth2Req{} if err := model.Decode(ctx, &meta); err != nil { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } state := utils.RandString(16) states.Store(state, stateMeta{ OAuth2Req: meta, }, time.Minute*5) ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ "url": pi.NewAuthURL(state), })) } // GET // /oauth2/callback/:type func OAuth2Callback(ctx *gin.Context) { code := ctx.Query("code") if code == "" { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("invalid oauth2 code")) return } pi, err := providers.GetProvider(provider.OAuth2Provider(ctx.Param("type"))) if err != nil { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } ld, err := login(ctx, ctx.Query("state"), code, pi) if err != nil { if err == op.ErrUserBanned || err == op.ErrUserPending { ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorResp(err)) return } ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } RenderToken(ctx, ld.redirect, ld.token) } // POST // /oauth2/callback/:type func OAuth2CallbackApi(ctx *gin.Context) { req := model.OAuth2CallbackReq{} if err := req.Decode(ctx); err != nil { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } pi, err := providers.GetProvider(provider.OAuth2Provider(ctx.Param("type"))) if err != nil { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) } ld, err := login(ctx, req.State, req.Code, pi) if err != nil { if err == op.ErrUserBanned || err == op.ErrUserPending { ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorResp(err)) return } ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ "token": ld.token, "redirect": ld.redirect, })) } type loginData struct { token, redirect string } func login(ctx context.Context, state, code string, pi provider.ProviderInterface) (*loginData, error) { meta, loaded := states.LoadAndDelete(state) if !loaded { return nil, errors.New("invalid oauth2 state") } t, err := pi.GetToken(ctx, code) if err != nil { return nil, err } ui, err := pi.GetUserInfo(ctx, t) if err != nil { return nil, err } pgs, loaded := bootstrap.ProviderGroupSettings[dbModel.SettingGroup(fmt.Sprintf("%s_%s", dbModel.SettingGroupOauth2, pi.Provider()))] if !loaded { return nil, errors.New("invalid oauth2 provider") } var user *op.User if meta.Value().BindUserId != "" { user, err = op.LoadOrInitUserByID(meta.Value().BindUserId) } else if settings.DisableUserSignup.Get() || pgs.DisableUserSignup.Get() { user, err = op.GetUserByProvider(pi.Provider(), ui.ProviderUserID) } else { if settings.SignupNeedReview.Get() || pgs.SignupNeedReview.Get() { user, err = op.CreateOrLoadUserWithProvider(ui.Username, utils.RandString(16), pi.Provider(), ui.ProviderUserID, db.WithRole(dbModel.RolePending)) } else { user, err = op.CreateOrLoadUserWithProvider(ui.Username, utils.RandString(16), pi.Provider(), ui.ProviderUserID) } } if err != nil { return nil, err } if meta.Value().BindUserId != "" { err = user.BindProvider(pi.Provider(), ui.ProviderUserID) if err != nil { return nil, err } } token, err := middlewares.NewAuthUserToken(user) if err != nil { return nil, err } return &loginData{ token: token, redirect: meta.Value().Redirect, }, nil }