✨ OAuth2 Support
This commit is contained in:
		@@ -14,6 +14,7 @@ func RunMigration(source *gorm.DB) error {
 | 
				
			|||||||
		&models.AuthSession{},
 | 
							&models.AuthSession{},
 | 
				
			||||||
		&models.AuthChallenge{},
 | 
							&models.AuthChallenge{},
 | 
				
			||||||
		&models.MagicToken{},
 | 
							&models.MagicToken{},
 | 
				
			||||||
 | 
							&models.ThirdClient{},
 | 
				
			||||||
	); err != nil {
 | 
						); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,17 +17,18 @@ const (
 | 
				
			|||||||
type Account struct {
 | 
					type Account struct {
 | 
				
			||||||
	BaseModel
 | 
						BaseModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Name        string                       `json:"name" gorm:"uniqueIndex"`
 | 
						Name         string                       `json:"name" gorm:"uniqueIndex"`
 | 
				
			||||||
	Nick        string                       `json:"nick"`
 | 
						Nick         string                       `json:"nick"`
 | 
				
			||||||
	State       AccountState                 `json:"state"`
 | 
						State        AccountState                 `json:"state"`
 | 
				
			||||||
	Profile     AccountProfile               `json:"profile"`
 | 
						Profile      AccountProfile               `json:"profile"`
 | 
				
			||||||
	Session     []AuthSession                `json:"sessions"`
 | 
						Sessions     []AuthSession                `json:"sessions"`
 | 
				
			||||||
	Challenges  []AuthChallenge              `json:"challenges"`
 | 
						Challenges   []AuthChallenge              `json:"challenges"`
 | 
				
			||||||
	Factors     []AuthFactor                 `json:"factors"`
 | 
						Factors      []AuthFactor                 `json:"factors"`
 | 
				
			||||||
	Contacts    []AccountContact             `json:"contacts"`
 | 
						Contacts     []AccountContact             `json:"contacts"`
 | 
				
			||||||
	MagicTokens []MagicToken                 `json:"-" gorm:"foreignKey:AssignTo"`
 | 
						MagicTokens  []MagicToken                 `json:"-" gorm:"foreignKey:AssignTo"`
 | 
				
			||||||
	ConfirmedAt *time.Time                   `json:"confirmed_at"`
 | 
						ThirdClients []ThirdClient                `json:"clients"`
 | 
				
			||||||
	Permissions datatypes.JSONType[[]string] `json:"permissions"`
 | 
						ConfirmedAt  *time.Time                   `json:"confirmed_at"`
 | 
				
			||||||
 | 
						Permissions  datatypes.JSONType[[]string] `json:"permissions"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (v Account) GetPrimaryEmail() AccountContact {
 | 
					func (v Account) GetPrimaryEmail() AccountContact {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,6 +27,7 @@ type AuthSession struct {
 | 
				
			|||||||
	BaseModel
 | 
						BaseModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Claims       datatypes.JSONSlice[string] `json:"claims"`
 | 
						Claims       datatypes.JSONSlice[string] `json:"claims"`
 | 
				
			||||||
 | 
						Audiences    datatypes.JSONSlice[string] `json:"audiences"`
 | 
				
			||||||
	Challenge    AuthChallenge               `json:"challenge" gorm:"foreignKey:SessionID"`
 | 
						Challenge    AuthChallenge               `json:"challenge" gorm:"foreignKey:SessionID"`
 | 
				
			||||||
	GrantToken   string                      `json:"grant_token"`
 | 
						GrantToken   string                      `json:"grant_token"`
 | 
				
			||||||
	AccessToken  string                      `json:"access_token"`
 | 
						AccessToken  string                      `json:"access_token"`
 | 
				
			||||||
@@ -34,6 +35,7 @@ type AuthSession struct {
 | 
				
			|||||||
	ExpiredAt    *time.Time                  `json:"expired_at"`
 | 
						ExpiredAt    *time.Time                  `json:"expired_at"`
 | 
				
			||||||
	AvailableAt  *time.Time                  `json:"available_at"`
 | 
						AvailableAt  *time.Time                  `json:"available_at"`
 | 
				
			||||||
	LastGrantAt  *time.Time                  `json:"last_grant_at"`
 | 
						LastGrantAt  *time.Time                  `json:"last_grant_at"`
 | 
				
			||||||
 | 
						ClientID     *uint                       `json:"client_id"`
 | 
				
			||||||
	AccountID    uint                        `json:"account_id"`
 | 
						AccountID    uint                        `json:"account_id"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -59,6 +61,7 @@ const (
 | 
				
			|||||||
type AuthChallenge struct {
 | 
					type AuthChallenge struct {
 | 
				
			||||||
	BaseModel
 | 
						BaseModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Location         string                     `json:"location"`
 | 
				
			||||||
	IpAddress        string                     `json:"ip_address"`
 | 
						IpAddress        string                     `json:"ip_address"`
 | 
				
			||||||
	UserAgent        string                     `json:"user_agent"`
 | 
						UserAgent        string                     `json:"user_agent"`
 | 
				
			||||||
	RiskLevel        int                        `json:"risk_level"`
 | 
						RiskLevel        int                        `json:"risk_level"`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,17 @@
 | 
				
			|||||||
package models
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type OauthClients struct {
 | 
					import "gorm.io/datatypes"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ThirdClient struct {
 | 
				
			||||||
 | 
						BaseModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Alias       string                      `json:"alias" gorm:"uniqueIndex"`
 | 
				
			||||||
 | 
						Name        string                      `json:"name"`
 | 
				
			||||||
 | 
						Description string                      `json:"description"`
 | 
				
			||||||
 | 
						Secret      string                      `json:"secret"`
 | 
				
			||||||
 | 
						Urls        datatypes.JSONSlice[string] `json:"urls"`
 | 
				
			||||||
 | 
						Callbacks   datatypes.JSONSlice[string] `json:"callbacks"`
 | 
				
			||||||
 | 
						Sessions    []AuthSession               `json:"sessions" gorm:"foreignKey:ClientID"`
 | 
				
			||||||
 | 
						IsDraft     bool                        `json:"is_draft"`
 | 
				
			||||||
 | 
						AccountID   *uint                       `json:"account_id"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,31 +10,36 @@ import (
 | 
				
			|||||||
	"gorm.io/datatypes"
 | 
						"gorm.io/datatypes"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func NewChallenge(account models.Account, factors []models.AuthFactor, ip, ua string) (models.AuthChallenge, error) {
 | 
					func CalcRisk(user models.Account, ip, ua string) int {
 | 
				
			||||||
	risk := 3
 | 
						risk := 3
 | 
				
			||||||
 | 
						var secureFactor int64
 | 
				
			||||||
 | 
						if err := database.C.Where(models.AuthChallenge{
 | 
				
			||||||
 | 
							AccountID: user.ID,
 | 
				
			||||||
 | 
							IpAddress: ip,
 | 
				
			||||||
 | 
						}).Model(models.AuthChallenge{}).Count(&secureFactor).Error; err == nil {
 | 
				
			||||||
 | 
							if secureFactor >= 3 {
 | 
				
			||||||
 | 
								risk -= 2
 | 
				
			||||||
 | 
							} else if secureFactor >= 1 {
 | 
				
			||||||
 | 
								risk -= 1
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return risk
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewChallenge(user models.Account, factors []models.AuthFactor, ip, ua string) (models.AuthChallenge, error) {
 | 
				
			||||||
	var challenge models.AuthChallenge
 | 
						var challenge models.AuthChallenge
 | 
				
			||||||
	// Pickup any challenge if possible
 | 
						// Pickup any challenge if possible
 | 
				
			||||||
	if err := database.C.Where(models.AuthChallenge{
 | 
						if err := database.C.Where(models.AuthChallenge{
 | 
				
			||||||
		AccountID: account.ID,
 | 
							AccountID: user.ID,
 | 
				
			||||||
	}).Where("state = ?", models.ActiveChallengeState).First(&challenge).Error; err == nil {
 | 
						}).Where("state = ?", models.ActiveChallengeState).First(&challenge).Error; err == nil {
 | 
				
			||||||
		return challenge, nil
 | 
							return challenge, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Reduce the risk level
 | 
						// Calculate the risk level
 | 
				
			||||||
	var secureFactor int64
 | 
						risk := CalcRisk(user, ip, ua)
 | 
				
			||||||
	if err := database.C.Where(models.AuthChallenge{
 | 
					 | 
				
			||||||
		AccountID: account.ID,
 | 
					 | 
				
			||||||
		IpAddress: ip,
 | 
					 | 
				
			||||||
	}).Model(models.AuthChallenge{}).Count(&secureFactor).Error; err != nil {
 | 
					 | 
				
			||||||
		return challenge, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if secureFactor >= 3 {
 | 
					 | 
				
			||||||
		risk -= 2
 | 
					 | 
				
			||||||
	} else if secureFactor >= 1 {
 | 
					 | 
				
			||||||
		risk -= 1
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Thinking of the requirements factors
 | 
						// Clamp risk in the exists requirements factor count
 | 
				
			||||||
	requirements := lo.Clamp(risk, 1, len(factors))
 | 
						requirements := lo.Clamp(risk, 1, len(factors))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	challenge = models.AuthChallenge{
 | 
						challenge = models.AuthChallenge{
 | 
				
			||||||
@@ -45,7 +50,7 @@ func NewChallenge(account models.Account, factors []models.AuthFactor, ip, ua st
 | 
				
			|||||||
		BlacklistFactors: datatypes.NewJSONType([]uint{}),
 | 
							BlacklistFactors: datatypes.NewJSONType([]uint{}),
 | 
				
			||||||
		State:            models.ActiveChallengeState,
 | 
							State:            models.ActiveChallengeState,
 | 
				
			||||||
		ExpiredAt:        time.Now().Add(2 * time.Hour),
 | 
							ExpiredAt:        time.Now().Add(2 * time.Hour),
 | 
				
			||||||
		AccountID:        account.ID,
 | 
							AccountID:        user.ID,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err := database.C.Save(&challenge).Error
 | 
						err := database.C.Save(&challenge).Error
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ import (
 | 
				
			|||||||
	"github.com/samber/lo"
 | 
						"github.com/samber/lo"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GrantSession(challenge models.AuthChallenge, claims []string, expired *time.Time, available *time.Time) (models.AuthSession, error) {
 | 
					func GrantSession(challenge models.AuthChallenge, claims, audiences []string, expired, available *time.Time) (models.AuthSession, error) {
 | 
				
			||||||
	var session models.AuthSession
 | 
						var session models.AuthSession
 | 
				
			||||||
	if err := challenge.IsAvailable(); err != nil {
 | 
						if err := challenge.IsAvailable(); err != nil {
 | 
				
			||||||
		return session, err
 | 
							return session, err
 | 
				
			||||||
@@ -24,6 +24,7 @@ func GrantSession(challenge models.AuthChallenge, claims []string, expired *time
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	session = models.AuthSession{
 | 
						session = models.AuthSession{
 | 
				
			||||||
		Claims:       claims,
 | 
							Claims:       claims,
 | 
				
			||||||
 | 
							Audiences:    audiences,
 | 
				
			||||||
		Challenge:    challenge,
 | 
							Challenge:    challenge,
 | 
				
			||||||
		GrantToken:   uuid.NewString(),
 | 
							GrantToken:   uuid.NewString(),
 | 
				
			||||||
		AccessToken:  uuid.NewString(),
 | 
							AccessToken:  uuid.NewString(),
 | 
				
			||||||
@@ -42,7 +43,42 @@ func GrantSession(challenge models.AuthChallenge, claims []string, expired *time
 | 
				
			|||||||
	return session, nil
 | 
						return session, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GetToken(session models.AuthSession, aud ...string) (string, string, error) {
 | 
					func GrantOauthSession(user models.Account, client models.ThirdClient, claims, audiences []string, expired, available *time.Time, ip, ua string) (models.AuthSession, error) {
 | 
				
			||||||
 | 
						session := models.AuthSession{
 | 
				
			||||||
 | 
							Claims:    claims,
 | 
				
			||||||
 | 
							Audiences: audiences,
 | 
				
			||||||
 | 
							Challenge: models.AuthChallenge{
 | 
				
			||||||
 | 
								IpAddress: ip,
 | 
				
			||||||
 | 
								UserAgent: ua,
 | 
				
			||||||
 | 
								RiskLevel: CalcRisk(user, ip, ua),
 | 
				
			||||||
 | 
								State:     models.FinishChallengeState,
 | 
				
			||||||
 | 
								AccountID: user.ID,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							GrantToken:   uuid.NewString(),
 | 
				
			||||||
 | 
							AccessToken:  uuid.NewString(),
 | 
				
			||||||
 | 
							RefreshToken: uuid.NewString(),
 | 
				
			||||||
 | 
							ExpiredAt:    expired,
 | 
				
			||||||
 | 
							AvailableAt:  available,
 | 
				
			||||||
 | 
							ClientID:     &client.ID,
 | 
				
			||||||
 | 
							AccountID:    user.ID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := database.C.Save(&session).Error; err != nil {
 | 
				
			||||||
 | 
							return session, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return session, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func RegenSession(session models.AuthSession) (models.AuthSession, error) {
 | 
				
			||||||
 | 
						session.GrantToken = uuid.NewString()
 | 
				
			||||||
 | 
						session.AccessToken = uuid.NewString()
 | 
				
			||||||
 | 
						session.RefreshToken = uuid.NewString()
 | 
				
			||||||
 | 
						err := database.C.Save(&session).Error
 | 
				
			||||||
 | 
						return session, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetToken(session models.AuthSession) (string, string, error) {
 | 
				
			||||||
	var refresh, access string
 | 
						var refresh, access string
 | 
				
			||||||
	if err := session.IsAvailable(); err != nil {
 | 
						if err := session.IsAvailable(); err != nil {
 | 
				
			||||||
		return refresh, access, err
 | 
							return refresh, access, err
 | 
				
			||||||
@@ -51,11 +87,11 @@ func GetToken(session models.AuthSession, aud ...string) (string, string, error)
 | 
				
			|||||||
	var err error
 | 
						var err error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	sub := strconv.Itoa(int(session.ID))
 | 
						sub := strconv.Itoa(int(session.ID))
 | 
				
			||||||
	access, err = EncodeJwt(session.AccessToken, nil, JwtAccessType, sub, aud, time.Now().Add(30*time.Minute))
 | 
						access, err = EncodeJwt(session.AccessToken, nil, JwtAccessType, sub, session.Audiences, time.Now().Add(30*time.Minute))
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return refresh, access, err
 | 
							return refresh, access, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	refresh, err = EncodeJwt(session.RefreshToken, nil, JwtRefreshType, sub, aud, time.Now().Add(30*24*time.Hour))
 | 
						refresh, err = EncodeJwt(session.RefreshToken, nil, JwtRefreshType, sub, session.Audiences, time.Now().Add(30*24*time.Hour))
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return refresh, access, err
 | 
							return refresh, access, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -66,7 +102,29 @@ func GetToken(session models.AuthSession, aud ...string) (string, string, error)
 | 
				
			|||||||
	return access, refresh, nil
 | 
						return access, refresh, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func ExchangeToken(token string, aud ...string) (string, string, error) {
 | 
					func ExchangeToken(token string) (string, string, error) {
 | 
				
			||||||
 | 
						var session models.AuthSession
 | 
				
			||||||
 | 
						if err := database.C.Where(models.AuthSession{GrantToken: token}).First(&session).Error; err != nil {
 | 
				
			||||||
 | 
							return "404", "403", err
 | 
				
			||||||
 | 
						} else if session.LastGrantAt != nil {
 | 
				
			||||||
 | 
							return "404", "403", fmt.Errorf("session was granted the first token, use refresh token instead")
 | 
				
			||||||
 | 
						} else if len(session.Audiences) > 1 {
 | 
				
			||||||
 | 
							return "404", "403", fmt.Errorf("should use authorization code grant type")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return GetToken(session)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ExchangeOauthToken(clientId, clientSecret, redirectUri, token string) (string, string, error) {
 | 
				
			||||||
 | 
						var client models.ThirdClient
 | 
				
			||||||
 | 
						if err := database.C.Where(models.ThirdClient{Alias: clientId}).First(&client).Error; err != nil {
 | 
				
			||||||
 | 
							return "404", "403", err
 | 
				
			||||||
 | 
						} else if client.Secret != clientSecret {
 | 
				
			||||||
 | 
							return "404", "403", fmt.Errorf("invalid client secret")
 | 
				
			||||||
 | 
						} else if !client.IsDraft && !lo.Contains(client.Callbacks, redirectUri) {
 | 
				
			||||||
 | 
							return "404", "403", fmt.Errorf("invalid redirect uri")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var session models.AuthSession
 | 
						var session models.AuthSession
 | 
				
			||||||
	if err := database.C.Where(models.AuthSession{GrantToken: token}).First(&session).Error; err != nil {
 | 
						if err := database.C.Where(models.AuthSession{GrantToken: token}).First(&session).Error; err != nil {
 | 
				
			||||||
		return "404", "403", err
 | 
							return "404", "403", err
 | 
				
			||||||
@@ -74,10 +132,10 @@ func ExchangeToken(token string, aud ...string) (string, string, error) {
 | 
				
			|||||||
		return "404", "403", fmt.Errorf("session was granted the first token, use refresh token instead")
 | 
							return "404", "403", fmt.Errorf("session was granted the first token, use refresh token instead")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return GetToken(session, aud...)
 | 
						return GetToken(session)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func RefreshToken(token string, aud ...string) (string, string, error) {
 | 
					func RefreshToken(token string) (string, string, error) {
 | 
				
			||||||
	parseInt := func(str string) int {
 | 
						parseInt := func(str string) int {
 | 
				
			||||||
		val, _ := strconv.Atoi(str)
 | 
							val, _ := strconv.Atoi(str)
 | 
				
			||||||
		return val
 | 
							return val
 | 
				
			||||||
@@ -94,5 +152,5 @@ func RefreshToken(token string, aud ...string) (string, string, error) {
 | 
				
			|||||||
		return "404", "403", err
 | 
							return "404", "403", err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return GetToken(session, aud...)
 | 
						return GetToken(session)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,16 +7,20 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"github.com/gofiber/fiber/v2"
 | 
						"github.com/gofiber/fiber/v2"
 | 
				
			||||||
	"github.com/spf13/viper"
 | 
						"github.com/spf13/viper"
 | 
				
			||||||
	"gorm.io/gorm/clause"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func getPrincipal(c *fiber.Ctx) error {
 | 
					func getPrincipal(c *fiber.Ctx) error {
 | 
				
			||||||
	user := c.Locals("principal").(models.Account)
 | 
						user := c.Locals("principal").(models.Account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var data models.Account
 | 
						var data models.Account
 | 
				
			||||||
	if err := database.C.Where(&models.Account{
 | 
						if err := database.C.
 | 
				
			||||||
		BaseModel: models.BaseModel{ID: user.ID},
 | 
							Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}).
 | 
				
			||||||
	}).Preload(clause.Associations).First(&data).Error; err != nil {
 | 
							Preload("Profile").
 | 
				
			||||||
 | 
							Preload("Contacts").
 | 
				
			||||||
 | 
							Preload("Factors").
 | 
				
			||||||
 | 
							Preload("Sessions").
 | 
				
			||||||
 | 
							Preload("Challenges").
 | 
				
			||||||
 | 
							First(&data).Error; err != nil {
 | 
				
			||||||
		return fiber.NewError(fiber.StatusInternalServerError, err.Error())
 | 
							return fiber.NewError(fiber.StatusInternalServerError, err.Error())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,7 +68,7 @@ func doChallenge(c *fiber.Ctx) error {
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fiber.NewError(fiber.StatusBadRequest, err.Error())
 | 
							return fiber.NewError(fiber.StatusBadRequest, err.Error())
 | 
				
			||||||
	} else if challenge.Progress >= challenge.Requirements {
 | 
						} else if challenge.Progress >= challenge.Requirements {
 | 
				
			||||||
		session, err := security.GrantSession(challenge, []string{"*"}, nil, lo.ToPtr(time.Now()))
 | 
							session, err := security.GrantSession(challenge, []string{"*"}, []string{"Hydrogen.Passport"}, nil, lo.ToPtr(time.Now()))
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return fiber.NewError(fiber.StatusBadRequest, err.Error())
 | 
								return fiber.NewError(fiber.StatusBadRequest, err.Error())
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -89,8 +89,11 @@ func doChallenge(c *fiber.Ctx) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func exchangeToken(c *fiber.Ctx) error {
 | 
					func exchangeToken(c *fiber.Ctx) error {
 | 
				
			||||||
	var data struct {
 | 
						var data struct {
 | 
				
			||||||
		Code      string `json:"code"`
 | 
							Code         string `json:"code" form:"code"`
 | 
				
			||||||
		GrantType string `json:"grant_type"`
 | 
							ClientID     string `json:"client_id" form:"client_id"`
 | 
				
			||||||
 | 
							ClientSecret string `json:"client_secret" form:"client_secret"`
 | 
				
			||||||
 | 
							RedirectUri  string `json:"redirect_uri" form:"redirect_uri"`
 | 
				
			||||||
 | 
							GrantType    string `json:"grant_type" form:"grant_type"`
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := BindAndValidate(c, &data); err != nil {
 | 
						if err := BindAndValidate(c, &data); err != nil {
 | 
				
			||||||
@@ -99,6 +102,18 @@ func exchangeToken(c *fiber.Ctx) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	switch data.GrantType {
 | 
						switch data.GrantType {
 | 
				
			||||||
	case "authorization_code":
 | 
						case "authorization_code":
 | 
				
			||||||
 | 
							// Authorization Code Mode
 | 
				
			||||||
 | 
							access, refresh, err := security.ExchangeOauthToken(data.ClientID, data.ClientSecret, data.RedirectUri, data.Code)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fiber.NewError(fiber.StatusBadRequest, err.Error())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return c.JSON(fiber.Map{
 | 
				
			||||||
 | 
								"access_token":  access,
 | 
				
			||||||
 | 
								"refresh_token": refresh,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						case "grant_token":
 | 
				
			||||||
 | 
							// Internal Usage
 | 
				
			||||||
		access, refresh, err := security.ExchangeToken(data.Code)
 | 
							access, refresh, err := security.ExchangeToken(data.Code)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return fiber.NewError(fiber.StatusBadRequest, err.Error())
 | 
								return fiber.NewError(fiber.StatusBadRequest, err.Error())
 | 
				
			||||||
@@ -109,6 +124,7 @@ func exchangeToken(c *fiber.Ctx) error {
 | 
				
			|||||||
			"refresh_token": refresh,
 | 
								"refresh_token": refresh,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	case "refresh_token":
 | 
						case "refresh_token":
 | 
				
			||||||
 | 
							// Refresh Token
 | 
				
			||||||
		access, refresh, err := security.RefreshToken(data.Code)
 | 
							access, refresh, err := security.RefreshToken(data.Code)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return fiber.NewError(fiber.StatusBadRequest, err.Error())
 | 
								return fiber.NewError(fiber.StatusBadRequest, err.Error())
 | 
				
			||||||
@@ -119,6 +135,6 @@ func exchangeToken(c *fiber.Ctx) error {
 | 
				
			|||||||
			"refresh_token": refresh,
 | 
								"refresh_token": refresh,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	default:
 | 
						default:
 | 
				
			||||||
		return fiber.NewError(fiber.StatusBadRequest, "Unsupported exchange token type.")
 | 
							return fiber.NewError(fiber.StatusBadRequest, "unsupported exchange token type")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										119
									
								
								pkg/server/oauth_api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								pkg/server/oauth_api.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					package server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.smartsheep.studio/hydrogen/passport/pkg/database"
 | 
				
			||||||
 | 
						"code.smartsheep.studio/hydrogen/passport/pkg/models"
 | 
				
			||||||
 | 
						"code.smartsheep.studio/hydrogen/passport/pkg/security"
 | 
				
			||||||
 | 
						"github.com/gofiber/fiber/v2"
 | 
				
			||||||
 | 
						"github.com/samber/lo"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func preConnect(c *fiber.Ctx) error {
 | 
				
			||||||
 | 
						id := c.Query("client_id")
 | 
				
			||||||
 | 
						redirect := c.Query("redirect_uri")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var client models.ThirdClient
 | 
				
			||||||
 | 
						if err := database.C.Where(&models.ThirdClient{Alias: id}).First(&client).Error; err != nil {
 | 
				
			||||||
 | 
							return fiber.NewError(fiber.StatusNotFound, err.Error())
 | 
				
			||||||
 | 
						} else if !client.IsDraft && !lo.Contains(client.Callbacks, strings.Split(redirect, "?")[0]) {
 | 
				
			||||||
 | 
							return fiber.NewError(fiber.StatusBadRequest, "invalid request url")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user := c.Locals("principal").(models.Account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var session models.AuthSession
 | 
				
			||||||
 | 
						if err := database.C.Where(&models.AuthSession{
 | 
				
			||||||
 | 
							AccountID: user.ID,
 | 
				
			||||||
 | 
							ClientID:  &client.ID,
 | 
				
			||||||
 | 
						}).First(&session).Error; err == nil {
 | 
				
			||||||
 | 
							if session.ExpiredAt != nil && session.ExpiredAt.Unix() < time.Now().Unix() {
 | 
				
			||||||
 | 
								return c.JSON(fiber.Map{
 | 
				
			||||||
 | 
									"client":  client,
 | 
				
			||||||
 | 
									"session": nil,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								session, err = security.RegenSession(session)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return c.JSON(fiber.Map{
 | 
				
			||||||
 | 
								"client":  client,
 | 
				
			||||||
 | 
								"session": session,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return c.JSON(fiber.Map{
 | 
				
			||||||
 | 
							"client":  client,
 | 
				
			||||||
 | 
							"session": nil,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func doConnect(c *fiber.Ctx) error {
 | 
				
			||||||
 | 
						user := c.Locals("principal").(models.Account)
 | 
				
			||||||
 | 
						id := c.Query("client_id")
 | 
				
			||||||
 | 
						response := c.Query("response_type")
 | 
				
			||||||
 | 
						redirect := c.Query("redirect_uri")
 | 
				
			||||||
 | 
						scope := c.Query("scope")
 | 
				
			||||||
 | 
						if len(scope) <= 0 {
 | 
				
			||||||
 | 
							return fiber.NewError(fiber.StatusBadRequest, "invalid request params")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var client models.ThirdClient
 | 
				
			||||||
 | 
						if err := database.C.Where(&models.ThirdClient{Alias: id}).First(&client).Error; err != nil {
 | 
				
			||||||
 | 
							return fiber.NewError(fiber.StatusNotFound, err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch response {
 | 
				
			||||||
 | 
						case "code":
 | 
				
			||||||
 | 
							// OAuth Authorization Mode
 | 
				
			||||||
 | 
							expired := time.Now().Add(7 * 24 * time.Hour)
 | 
				
			||||||
 | 
							session, err := security.GrantOauthSession(
 | 
				
			||||||
 | 
								user,
 | 
				
			||||||
 | 
								client,
 | 
				
			||||||
 | 
								strings.Split(scope, " "),
 | 
				
			||||||
 | 
								[]string{"Hydrogen.Passport", client.Alias},
 | 
				
			||||||
 | 
								&expired,
 | 
				
			||||||
 | 
								lo.ToPtr(time.Now()),
 | 
				
			||||||
 | 
								c.IP(),
 | 
				
			||||||
 | 
								c.Get(fiber.HeaderUserAgent),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fiber.NewError(fiber.StatusInternalServerError, err.Error())
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return c.JSON(fiber.Map{
 | 
				
			||||||
 | 
									"session":      session,
 | 
				
			||||||
 | 
									"redirect_uri": redirect,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case "token":
 | 
				
			||||||
 | 
							// OAuth Implicit Mode
 | 
				
			||||||
 | 
							expired := time.Now().Add(7 * 24 * time.Hour)
 | 
				
			||||||
 | 
							session, err := security.GrantOauthSession(
 | 
				
			||||||
 | 
								user,
 | 
				
			||||||
 | 
								client,
 | 
				
			||||||
 | 
								strings.Split(scope, " "),
 | 
				
			||||||
 | 
								[]string{"Hydrogen.Passport", client.Alias},
 | 
				
			||||||
 | 
								&expired,
 | 
				
			||||||
 | 
								lo.ToPtr(time.Now()),
 | 
				
			||||||
 | 
								c.IP(),
 | 
				
			||||||
 | 
								c.Get(fiber.HeaderUserAgent),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fiber.NewError(fiber.StatusInternalServerError, err.Error())
 | 
				
			||||||
 | 
							} else if access, refresh, err := security.GetToken(session); err != nil {
 | 
				
			||||||
 | 
								return fiber.NewError(fiber.StatusInternalServerError, err.Error())
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return c.JSON(fiber.Map{
 | 
				
			||||||
 | 
									"access_token":  access,
 | 
				
			||||||
 | 
									"refresh_token": refresh,
 | 
				
			||||||
 | 
									"redirect_uri":  redirect,
 | 
				
			||||||
 | 
									"session":       session,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return fiber.NewError(fiber.StatusBadRequest, "unsupported response type")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -31,6 +31,9 @@ func NewServer() {
 | 
				
			|||||||
		api.Post("/auth", doChallenge)
 | 
							api.Post("/auth", doChallenge)
 | 
				
			||||||
		api.Post("/auth/token", exchangeToken)
 | 
							api.Post("/auth/token", exchangeToken)
 | 
				
			||||||
		api.Post("/auth/factors/:factorId", requestFactorToken)
 | 
							api.Post("/auth/factors/:factorId", requestFactorToken)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							api.Get("/auth/oauth/connect", auth, preConnect)
 | 
				
			||||||
 | 
							api.Post("/auth/oauth/connect", auth, doConnect)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,6 +21,8 @@ render(() => (
 | 
				
			|||||||
        <Route path="/" component={lazy(() => import("./pages/dashboard.tsx"))} />
 | 
					        <Route path="/" component={lazy(() => import("./pages/dashboard.tsx"))} />
 | 
				
			||||||
        <Route path="/auth/login" component={lazy(() => import("./pages/auth/login.tsx"))} />
 | 
					        <Route path="/auth/login" component={lazy(() => import("./pages/auth/login.tsx"))} />
 | 
				
			||||||
        <Route path="/auth/register" component={lazy(() => import("./pages/auth/register.tsx"))} />
 | 
					        <Route path="/auth/register" component={lazy(() => import("./pages/auth/register.tsx"))} />
 | 
				
			||||||
 | 
					        <Route path="/auth/oauth/connect" component={lazy(() => import("./pages/auth/connect.tsx"))} />
 | 
				
			||||||
 | 
					        <Route path="/auth/oauth/callback" component={lazy(() => import("./pages/auth/callback.tsx"))} />
 | 
				
			||||||
        <Route path="/users/me/confirm" component={lazy(() => import("./pages/users/confirm.tsx"))} />
 | 
					        <Route path="/users/me/confirm" component={lazy(() => import("./pages/users/confirm.tsx"))} />
 | 
				
			||||||
      </Router>
 | 
					      </Router>
 | 
				
			||||||
    </UserinfoProvider>
 | 
					    </UserinfoProvider>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										30
									
								
								view/src/pages/auth/callback.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								view/src/pages/auth/callback.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					import { useSearchParams } from "@solidjs/router";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function DefaultCallbackPage() {
 | 
				
			||||||
 | 
					  const [searchParams] = useSearchParams();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div class="w-full h-full flex justify-center items-center">
 | 
				
			||||||
 | 
					      <div class="card w-[480px] max-w-screen shadow-xl">
 | 
				
			||||||
 | 
					        <div class="card-body">
 | 
				
			||||||
 | 
					          <div id="header" class="text-center mb-5">
 | 
				
			||||||
 | 
					            {/* Just Kidding */}
 | 
				
			||||||
 | 
					            <h1 class="text-xl font-bold">Default Callback</h1>
 | 
				
			||||||
 | 
					            <p>
 | 
				
			||||||
 | 
					              If you see this page, it means some genius developer forgot to set the redirect address, so you visited
 | 
				
			||||||
 | 
					              this default callback address.
 | 
				
			||||||
 | 
					              General Douglas MacArthur, a five-star general in the United States, commented on this: "If I let my
 | 
				
			||||||
 | 
					              soldiers use default callbacks, they'd rather die."
 | 
				
			||||||
 | 
					              The large documentary film "Callback Legend" is currently in theaters.
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div class="text-center">
 | 
				
			||||||
 | 
					            <p>Authorization Code</p>
 | 
				
			||||||
 | 
					            <code>{searchParams["code"]}</code>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										140
									
								
								view/src/pages/auth/connect.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								view/src/pages/auth/connect.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,140 @@
 | 
				
			|||||||
 | 
					import { createSignal, Show } from "solid-js";
 | 
				
			||||||
 | 
					import { useLocation, useSearchParams } from "@solidjs/router";
 | 
				
			||||||
 | 
					import { getAtk, useUserinfo } from "../../stores/userinfo.tsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function OauthConnectPage() {
 | 
				
			||||||
 | 
					  const [title, setTitle] = createSignal("Connect Third-party");
 | 
				
			||||||
 | 
					  const [subtitle, setSubtitle] = createSignal("Via your Goatpass account");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [error, setError] = createSignal<string | null>(null);
 | 
				
			||||||
 | 
					  const [status, setStatus] = createSignal("Handshaking...");
 | 
				
			||||||
 | 
					  const [loading, setLoading] = createSignal(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [client, setClient] = createSignal<any>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [searchParams] = useSearchParams();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const userinfo = useUserinfo();
 | 
				
			||||||
 | 
					  const location = useLocation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function preConnect() {
 | 
				
			||||||
 | 
					    const res = await fetch(`/api/auth/oauth/connect${location.search}`, {
 | 
				
			||||||
 | 
					      headers: { "Authorization": `Bearer ${getAtk()}` }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (res.status !== 200) {
 | 
				
			||||||
 | 
					      setError(await res.text());
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      const data = await res.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (data["session"]) {
 | 
				
			||||||
 | 
					        setStatus("Redirecting...");
 | 
				
			||||||
 | 
					        redirect(data["session"]);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        setTitle(`Connect ${data["client"].name}`);
 | 
				
			||||||
 | 
					        setSubtitle(`Via ${userinfo?.displayName}`);
 | 
				
			||||||
 | 
					        setClient(data["client"]);
 | 
				
			||||||
 | 
					        setLoading(false);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function decline() {
 | 
				
			||||||
 | 
					    if (window.history.length > 0) {
 | 
				
			||||||
 | 
					      window.history.back();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      window.close();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function approve() {
 | 
				
			||||||
 | 
					    setLoading(true);
 | 
				
			||||||
 | 
					    setStatus("Approving...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const res = await fetch("/api/auth/oauth/connect?" + new URLSearchParams({
 | 
				
			||||||
 | 
					      client_id: searchParams["client_id"] as string,
 | 
				
			||||||
 | 
					      redirect_uri: encodeURIComponent(searchParams["redirect_uri"] as string),
 | 
				
			||||||
 | 
					      response_type: "code",
 | 
				
			||||||
 | 
					      scope: searchParams["scope"] as string
 | 
				
			||||||
 | 
					    }), {
 | 
				
			||||||
 | 
					      method: "POST",
 | 
				
			||||||
 | 
					      headers: { "Authorization": `Bearer ${getAtk()}` }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (res.status !== 200) {
 | 
				
			||||||
 | 
					      setError(await res.text());
 | 
				
			||||||
 | 
					      setLoading(false);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      const data = await res.json();
 | 
				
			||||||
 | 
					      setStatus("Redirecting...");
 | 
				
			||||||
 | 
					      setTimeout(() => redirect(data["session"]), 1850);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function redirect(session: any) {
 | 
				
			||||||
 | 
					    const url = `${searchParams["redirect_uri"]}?code=${session["grant_token"]}&state=${searchParams["state"]}`;
 | 
				
			||||||
 | 
					    window.open(url, "_self");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  preConnect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div class="w-full h-full flex justify-center items-center">
 | 
				
			||||||
 | 
					      <div class="card w-[480px] max-w-screen shadow-xl">
 | 
				
			||||||
 | 
					        <div class="card-body">
 | 
				
			||||||
 | 
					          <div id="header" class="text-center mb-5">
 | 
				
			||||||
 | 
					            <h1 class="text-xl font-bold">{title()}</h1>
 | 
				
			||||||
 | 
					            <p>{subtitle()}</p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <Show when={error()}>
 | 
				
			||||||
 | 
					            <div id="alerts" class="mt-1">
 | 
				
			||||||
 | 
					              <div role="alert" class="alert alert-error">
 | 
				
			||||||
 | 
					                <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
 | 
				
			||||||
 | 
					                     viewBox="0 0 24 24">
 | 
				
			||||||
 | 
					                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
 | 
				
			||||||
 | 
					                        d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
 | 
				
			||||||
 | 
					                </svg>
 | 
				
			||||||
 | 
					                <span class="capitalize">{error()}</span>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </Show>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <Show when={loading()}>
 | 
				
			||||||
 | 
					            <div class="py-16 text-center">
 | 
				
			||||||
 | 
					              <div class="text-center">
 | 
				
			||||||
 | 
					                <div>
 | 
				
			||||||
 | 
					                  <span class="loading loading-lg loading-bars"></span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <span>{status()}</span>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </Show>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <Show when={!loading()}>
 | 
				
			||||||
 | 
					            <div class="mb-3">
 | 
				
			||||||
 | 
					              <h2 class="font-bold">About who you connecting to</h2>
 | 
				
			||||||
 | 
					              <p>{client().description}</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="mb-3">
 | 
				
			||||||
 | 
					              <h2 class="font-bold">Make sure you trust them</h2>
 | 
				
			||||||
 | 
					              <p>You may share your personal information after connect them. Learn about their privacy policy and user
 | 
				
			||||||
 | 
					                agreement to keep your personal information in safe.</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="mb-5">
 | 
				
			||||||
 | 
					              <h2 class="font-bold">After approve this request</h2>
 | 
				
			||||||
 | 
					              <p>
 | 
				
			||||||
 | 
					                You will be redirect to{" "}
 | 
				
			||||||
 | 
					                <span class="link link-primary cursor-not-allowed">{searchParams["redirect_uri"]}</span>
 | 
				
			||||||
 | 
					              </p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="grid grid-cols-1 md:grid-cols-2">
 | 
				
			||||||
 | 
					              <button class="btn btn-accent" onClick={() => decline()}>Decline</button>
 | 
				
			||||||
 | 
					              <button class="btn btn-primary" onClick={() => approve()}>Approve</button>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </Show>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -108,7 +108,7 @@ export default function LoginPage() {
 | 
				
			|||||||
      headers: { "Content-Type": "application/json" },
 | 
					      headers: { "Content-Type": "application/json" },
 | 
				
			||||||
      body: JSON.stringify({
 | 
					      body: JSON.stringify({
 | 
				
			||||||
        code: tk,
 | 
					        code: tk,
 | 
				
			||||||
        grant_type: "authorization_code"
 | 
					        grant_type: "grant_token"
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    if (res.status !== 200) {
 | 
					    if (res.status !== 200) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ export default function DashboardPage() {
 | 
				
			|||||||
  const userinfo = useUserinfo();
 | 
					  const userinfo = useUserinfo();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div class="container mx-auto pt-12">
 | 
					    <div class="max-w-[720px] mx-auto px-5 pt-12">
 | 
				
			||||||
      <h1 class="text-2xl font-bold">Welcome, {userinfo?.displayName}</h1>
 | 
					      <h1 class="text-2xl font-bold">Welcome, {userinfo?.displayName}</h1>
 | 
				
			||||||
      <p>What's a nice day!</p>
 | 
					      <p>What's a nice day!</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user