mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-10-28 12:45:47 +00:00
Compare commits
19 Commits
v1.0.0-bet
...
v2.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c54267f50d | ||
|
|
4de12ce5c1 | ||
|
|
0cf0aafc14 | ||
|
|
80ea43184c | ||
|
|
3c4dffd479 | ||
|
|
f19f40f9fc | ||
|
|
a243f22ac8 | ||
|
|
08d382c981 | ||
|
|
94f7debb10 | ||
|
|
3b50d9303b | ||
|
|
d67133aca7 | ||
|
|
989ea8f229 | ||
|
|
708006decf | ||
|
|
682a918812 | ||
|
|
389248cfe1 | ||
|
|
81d25061df | ||
|
|
f59697955d | ||
|
|
47d8f1e5aa | ||
|
|
e8d2e059a9 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -8,4 +8,8 @@ tinyauth
|
||||
docker-compose.test.yml
|
||||
|
||||
# users file
|
||||
users.txt
|
||||
users.txt
|
||||
|
||||
# secret test file
|
||||
secret.txt
|
||||
secret_oauth.txt
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<br />
|
||||
|
||||
Tinyauth is a simple authentication middleware that adds simple email/password login to all of your docker apps. It is made for traefik but it can be extended to work with all reverse proxies like caddy and nginx.
|
||||
Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps. It is made for traefik but it can be extended to work with all reverse proxies like caddy and nginx.
|
||||
|
||||
> [!WARNING]
|
||||
> Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating.
|
||||
|
||||
65
cmd/root.go
65
cmd/root.go
@@ -1,6 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
cmd "tinyauth/cmd/user"
|
||||
"tinyauth/internal/api"
|
||||
"tinyauth/internal/auth"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
@@ -17,8 +19,8 @@ import (
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "tinyauth",
|
||||
Short: "An extremely simple traefik forward auth proxy.",
|
||||
Long: `Tinyauth is an extremely simple traefik forward-auth login screen that makes securing your apps easy.`,
|
||||
Short: "The simplest way to protect your apps with a login screen.",
|
||||
Long: `Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Get config
|
||||
log.Info().Msg("Parsing config")
|
||||
@@ -26,38 +28,35 @@ var rootCmd = &cobra.Command{
|
||||
parseErr := viper.Unmarshal(&config)
|
||||
HandleError(parseErr, "Failed to parse config")
|
||||
|
||||
// Secrets
|
||||
log.Info().Msg("Parsing secrets")
|
||||
|
||||
config.Secret = utils.GetSecret(config.Secret, config.SecretFile)
|
||||
config.GithubClientSecret = utils.GetSecret(config.GithubClientSecret, config.GithubClientSecretFile)
|
||||
config.GoogleClientSecret = utils.GetSecret(config.GoogleClientSecret, config.GoogleClientSecretFile)
|
||||
config.GenericClientSecret = utils.GetSecret(config.GenericClientSecret, config.GenericClientSecretFile)
|
||||
|
||||
// Validate config
|
||||
log.Info().Msg("Validating config")
|
||||
validator := validator.New()
|
||||
validateErr := validator.Struct(config)
|
||||
HandleError(validateErr, "Invalid config")
|
||||
|
||||
// Parse users
|
||||
// Set log level
|
||||
log.Info().Int8("log_level", config.LogLevel).Msg("Setting log level")
|
||||
log.Logger = log.Logger.Level(zerolog.Level(config.LogLevel))
|
||||
|
||||
// Users
|
||||
log.Info().Msg("Parsing users")
|
||||
users, usersErr := utils.GetUsers(config.Users, config.UsersFile)
|
||||
|
||||
if config.UsersFile == "" && config.Users == "" {
|
||||
log.Fatal().Msg("No users provided")
|
||||
if (len(users) == 0 || usersErr != nil) && !utils.OAuthConfigured(config) {
|
||||
log.Fatal().Err(usersErr).Msg("Failed to parse users")
|
||||
}
|
||||
|
||||
usersString := config.Users
|
||||
|
||||
if config.UsersFile != "" {
|
||||
log.Info().Msg("Reading users from file")
|
||||
usersFromFile, readErr := utils.GetUsersFromFile(config.UsersFile)
|
||||
HandleError(readErr, "Failed to read users from file")
|
||||
usersFromFileParsed := utils.ParseFileToLine(usersFromFile)
|
||||
if usersString != "" {
|
||||
usersString = usersString + "," + usersFromFileParsed
|
||||
} else {
|
||||
usersString = usersFromFileParsed
|
||||
}
|
||||
}
|
||||
|
||||
users, parseErr := utils.ParseUsers(usersString)
|
||||
HandleError(parseErr, "Failed to parse users")
|
||||
|
||||
// Create oauth whitelist
|
||||
oauthWhitelist := utils.ParseCommaString(config.OAuthWhitelist)
|
||||
oauthWhitelist := strings.Split(config.OAuthWhitelist, ",")
|
||||
log.Debug().Strs("oauth_whitelist", oauthWhitelist).Msg("Parsed OAuth whitelist")
|
||||
|
||||
// Create OAuth config
|
||||
oauthConfig := types.OAuthConfig{
|
||||
@@ -67,12 +66,13 @@ var rootCmd = &cobra.Command{
|
||||
GoogleClientSecret: config.GoogleClientSecret,
|
||||
GenericClientId: config.GenericClientId,
|
||||
GenericClientSecret: config.GenericClientSecret,
|
||||
GenericScopes: utils.ParseCommaString(config.GenericScopes),
|
||||
GenericScopes: strings.Split(config.GenericScopes, ","),
|
||||
GenericAuthURL: config.GenericAuthURL,
|
||||
GenericTokenURL: config.GenericTokenURL,
|
||||
GenericUserURL: config.GenericUserURL,
|
||||
AppURL: config.AppURL,
|
||||
}
|
||||
log.Debug().Interface("oauth_config", oauthConfig).Msg("Parsed OAuth config")
|
||||
|
||||
// Create auth service
|
||||
auth := auth.NewAuth(users, oauthWhitelist)
|
||||
@@ -94,6 +94,7 @@ var rootCmd = &cobra.Command{
|
||||
AppURL: config.AppURL,
|
||||
CookieSecure: config.CookieSecure,
|
||||
DisableContinue: config.DisableContinue,
|
||||
CookieExpiry: config.CookieExpiry,
|
||||
}, hooks, auth, providers)
|
||||
|
||||
// Setup routes
|
||||
@@ -124,16 +125,20 @@ func init() {
|
||||
rootCmd.Flags().Int("port", 3000, "Port to run the server on.")
|
||||
rootCmd.Flags().String("address", "0.0.0.0", "Address to bind the server to.")
|
||||
rootCmd.Flags().String("secret", "", "Secret to use for the cookie.")
|
||||
rootCmd.Flags().String("secret-file", "", "Path to a file containing the secret.")
|
||||
rootCmd.Flags().String("app-url", "", "The tinyauth URL.")
|
||||
rootCmd.Flags().String("users", "", "Comma separated list of users in the format username:bcrypt-hashed-password.")
|
||||
rootCmd.Flags().String("users-file", "", "Path to a file containing users in the format username:bcrypt-hashed-password.")
|
||||
rootCmd.Flags().String("users", "", "Comma separated list of users in the format username:hash.")
|
||||
rootCmd.Flags().String("users-file", "", "Path to a file containing users in the format username:hash.")
|
||||
rootCmd.Flags().Bool("cookie-secure", false, "Send cookie over secure connection only.")
|
||||
rootCmd.Flags().String("github-client-id", "", "Github OAuth client ID.")
|
||||
rootCmd.Flags().String("github-client-secret", "", "Github OAuth client secret.")
|
||||
rootCmd.Flags().String("github-client-secret-file", "", "Github OAuth client secret file.")
|
||||
rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.")
|
||||
rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.")
|
||||
rootCmd.Flags().String("google-client-secret-file", "", "Google OAuth client secret file.")
|
||||
rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.")
|
||||
rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.")
|
||||
rootCmd.Flags().String("generic-client-secret-file", "", "Generic OAuth client secret file.")
|
||||
rootCmd.Flags().String("generic-scopes", "", "Generic OAuth scopes.")
|
||||
rootCmd.Flags().String("generic-auth-url", "", "Generic OAuth auth URL.")
|
||||
rootCmd.Flags().String("generic-token-url", "", "Generic OAuth token URL.")
|
||||
@@ -141,25 +146,31 @@ func init() {
|
||||
rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
|
||||
rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
|
||||
rootCmd.Flags().Int("cookie-expiry", 86400, "Cookie expiration time in seconds.")
|
||||
rootCmd.Flags().Int("log-level", 1, "Log level.")
|
||||
viper.BindEnv("port", "PORT")
|
||||
viper.BindEnv("address", "ADDRESS")
|
||||
viper.BindEnv("secret", "SECRET")
|
||||
viper.BindEnv("secret-file", "SECRET_FILE")
|
||||
viper.BindEnv("app-url", "APP_URL")
|
||||
viper.BindEnv("users", "USERS")
|
||||
viper.BindEnv("users-file", "USERS_FILE")
|
||||
viper.BindEnv("cookie-secure", "COOKIE_SECURE")
|
||||
viper.BindEnv("github-client-id", "GITHUB_CLIENT_ID")
|
||||
viper.BindEnv("github-client-secret", "GITHUB_CLIENT_SECRET")
|
||||
viper.BindEnv("github-client-secret-file", "GITHUB_CLIENT_SECRET_FILE")
|
||||
viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID")
|
||||
viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET")
|
||||
viper.BindEnv("google-client-secret-file", "GOOGLE_CLIENT_SECRET_FILE")
|
||||
viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID")
|
||||
viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET")
|
||||
viper.BindEnv("generic-client-secret-file", "GENERIC_CLIENT_SECRET_FILE")
|
||||
viper.BindEnv("generic-scopes", "GENERIC_SCOPES")
|
||||
viper.BindEnv("generic-auth-url", "GENERIC_AUTH_URL")
|
||||
viper.BindEnv("generic-token-url", "GENERIC_TOKEN_URL")
|
||||
viper.BindEnv("generic-user-url", "GENERIC_USER_URL")
|
||||
viper.BindEnv("disable-continue", "DISABLE_CONTINUE")
|
||||
viper.BindEnv("oauth-whitelist", "WHITELIST")
|
||||
viper.BindEnv("oauth-whitelist", "OAUTH_WHITELIST")
|
||||
viper.BindEnv("cookie-expiry", "COOKIE_EXPIRY")
|
||||
viper.BindEnv("log-level", "LOG_LEVEL")
|
||||
viper.BindPFlags(rootCmd.Flags())
|
||||
}
|
||||
|
||||
@@ -19,12 +19,12 @@ var user string
|
||||
var VerifyCmd = &cobra.Command{
|
||||
Use: "verify",
|
||||
Short: "Verify a user is set up correctly",
|
||||
Long: `Verify a user is set up correctly meaning that it has a correct password.`,
|
||||
Long: `Verify a user is set up correctly meaning that it has a correct username and password.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if interactive {
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("User (user:hash)").Value(&user).Validate((func(s string) error {
|
||||
huh.NewInput().Title("User (username:hash)").Value(&user).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("user cannot be empty")
|
||||
}
|
||||
@@ -86,5 +86,5 @@ func init() {
|
||||
VerifyCmd.Flags().BoolVar(&docker, "docker", false, "Is the user formatted for docker?")
|
||||
VerifyCmd.Flags().StringVar(&username, "username", "", "Username")
|
||||
VerifyCmd.Flags().StringVar(&password, "password", "", "Password")
|
||||
VerifyCmd.Flags().StringVar(&user, "user", "", "Hash (user:hash combination)")
|
||||
VerifyCmd.Flags().StringVar(&user, "user", "", "Hash (username:hash combination)")
|
||||
}
|
||||
|
||||
@@ -42,17 +42,22 @@ type API struct {
|
||||
func (api *API) Init() {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
log.Debug().Msg("Setting up router")
|
||||
router := gin.New()
|
||||
router.Use(zerolog())
|
||||
log.Debug().Msg("Setting up assets")
|
||||
dist, distErr := fs.Sub(assets.Assets, "dist")
|
||||
|
||||
if distErr != nil {
|
||||
log.Fatal().Err(distErr).Msg("Failed to get UI assets")
|
||||
}
|
||||
|
||||
log.Debug().Msg("Setting up file server")
|
||||
fileServer := http.FileServer(http.FS(dist))
|
||||
log.Debug().Msg("Setting up cookie store")
|
||||
store := cookie.NewStore([]byte(api.Config.Secret))
|
||||
|
||||
log.Debug().Msg("Getting domain")
|
||||
domain, domainErr := utils.GetRootURL(api.Config.AppURL)
|
||||
|
||||
if domainErr != nil {
|
||||
@@ -90,18 +95,11 @@ func (api *API) Init() {
|
||||
|
||||
func (api *API) SetupRoutes() {
|
||||
api.Router.GET("/api/auth", func(c *gin.Context) {
|
||||
userContext, userContextErr := api.Hooks.UseUserContext(c)
|
||||
|
||||
if userContextErr != nil {
|
||||
log.Error().Err(userContextErr).Msg("Failed to get user context")
|
||||
c.JSON(500, gin.H{
|
||||
"status": 500,
|
||||
"message": "Internal Server Error",
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Debug().Msg("Checking auth")
|
||||
userContext := api.Hooks.UseUserContext(c)
|
||||
|
||||
if userContext.IsLoggedIn {
|
||||
log.Debug().Msg("Authenticated")
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"message": "Authenticated",
|
||||
@@ -116,6 +114,8 @@ func (api *API) SetupRoutes() {
|
||||
RedirectURI: fmt.Sprintf("%s://%s%s", proto, host, uri),
|
||||
})
|
||||
|
||||
log.Debug().Interface("queries", queries).Msg("Redirecting to login")
|
||||
|
||||
if queryErr != nil {
|
||||
log.Error().Err(queryErr).Msg("Failed to build query")
|
||||
c.JSON(501, gin.H{
|
||||
@@ -142,9 +142,12 @@ func (api *API) SetupRoutes() {
|
||||
return
|
||||
}
|
||||
|
||||
user := api.Auth.GetUser(login.Email)
|
||||
log.Debug().Interface("login", login).Msg("Got login request")
|
||||
|
||||
user := api.Auth.GetUser(login.Username)
|
||||
|
||||
if user == nil {
|
||||
log.Debug().Str("username", login.Username).Msg("User not found")
|
||||
c.JSON(401, gin.H{
|
||||
"status": 401,
|
||||
"message": "Unauthorized",
|
||||
@@ -153,6 +156,7 @@ func (api *API) SetupRoutes() {
|
||||
}
|
||||
|
||||
if !api.Auth.CheckPassword(*user, login.Password) {
|
||||
log.Debug().Str("username", login.Username).Msg("Password incorrect")
|
||||
c.JSON(401, gin.H{
|
||||
"status": 401,
|
||||
"message": "Unauthorized",
|
||||
@@ -160,9 +164,12 @@ func (api *API) SetupRoutes() {
|
||||
return
|
||||
}
|
||||
|
||||
session := sessions.Default(c)
|
||||
session.Set("tinyauth_sid", fmt.Sprintf("email:%s", login.Email))
|
||||
session.Save()
|
||||
log.Debug().Msg("Password correct, logging in")
|
||||
|
||||
api.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
||||
Username: login.Username,
|
||||
Provider: "username",
|
||||
})
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
@@ -171,9 +178,9 @@ func (api *API) SetupRoutes() {
|
||||
})
|
||||
|
||||
api.Router.POST("/api/logout", func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Delete("tinyauth_sid")
|
||||
session.Save()
|
||||
api.Auth.DeleteSessionCookie(c)
|
||||
|
||||
log.Debug().Msg("Cleaning up redirect cookie")
|
||||
|
||||
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", api.Domain, api.Config.CookieSecure, true)
|
||||
|
||||
@@ -184,39 +191,40 @@ func (api *API) SetupRoutes() {
|
||||
})
|
||||
|
||||
api.Router.GET("/api/status", func(c *gin.Context) {
|
||||
userContext, userContextErr := api.Hooks.UseUserContext(c)
|
||||
log.Debug().Msg("Checking status")
|
||||
userContext := api.Hooks.UseUserContext(c)
|
||||
|
||||
if userContextErr != nil {
|
||||
log.Error().Err(userContextErr).Msg("Failed to get user context")
|
||||
c.JSON(500, gin.H{
|
||||
"status": 500,
|
||||
"message": "Internal Server Error",
|
||||
})
|
||||
return
|
||||
configuredProviders := api.Providers.GetConfiguredProviders()
|
||||
|
||||
if api.Auth.UserAuthConfigured() {
|
||||
configuredProviders = append(configuredProviders, "username")
|
||||
}
|
||||
|
||||
if !userContext.IsLoggedIn {
|
||||
log.Debug().Msg("Unauthenticated")
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"message": "Unauthenticated",
|
||||
"email": "",
|
||||
"username": "",
|
||||
"isLoggedIn": false,
|
||||
"oauth": false,
|
||||
"provider": "",
|
||||
"configuredProviders": api.Providers.GetConfiguredProviders(),
|
||||
"configuredProviders": configuredProviders,
|
||||
"disableContinue": api.Config.DisableContinue,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Interface("userContext", userContext).Strs("configuredProviders", configuredProviders).Bool("disableContinue", api.Config.DisableContinue).Msg("Authenticated")
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"message": "Authenticated",
|
||||
"email": userContext.Email,
|
||||
"username": userContext.Username,
|
||||
"isLoggedIn": userContext.IsLoggedIn,
|
||||
"oauth": userContext.OAuth,
|
||||
"provider": userContext.Provider,
|
||||
"configuredProviders": api.Providers.GetConfiguredProviders(),
|
||||
"configuredProviders": configuredProviders,
|
||||
"disableContinue": api.Config.DisableContinue,
|
||||
})
|
||||
})
|
||||
@@ -242,6 +250,8 @@ func (api *API) SetupRoutes() {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Interface("request", request).Msg("Got OAuth request")
|
||||
|
||||
provider := api.Providers.GetProvider(request.Provider)
|
||||
|
||||
if provider == nil {
|
||||
@@ -252,11 +262,16 @@ func (api *API) SetupRoutes() {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Str("provider", request.Provider).Msg("Got provider")
|
||||
|
||||
authURL := provider.GetAuthURL()
|
||||
|
||||
log.Debug().Str("authURL", authURL).Msg("Got auth URL")
|
||||
|
||||
redirectURI := c.Query("redirect_uri")
|
||||
|
||||
if redirectURI != "" {
|
||||
log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie")
|
||||
c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", api.Domain, api.Config.CookieSecure, true)
|
||||
}
|
||||
|
||||
@@ -276,6 +291,8 @@ func (api *API) SetupRoutes() {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Interface("providerName", providerName).Msg("Got provider name")
|
||||
|
||||
code := c.Query("code")
|
||||
|
||||
if code == "" {
|
||||
@@ -284,8 +301,12 @@ func (api *API) SetupRoutes() {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Str("code", code).Msg("Got code")
|
||||
|
||||
provider := api.Providers.GetProvider(providerName.Provider)
|
||||
|
||||
log.Debug().Str("provider", providerName.Provider).Msg("Got provider")
|
||||
|
||||
if provider == nil {
|
||||
c.Redirect(http.StatusPermanentRedirect, "/not-found")
|
||||
return
|
||||
@@ -293,12 +314,16 @@ func (api *API) SetupRoutes() {
|
||||
|
||||
token, tokenErr := provider.ExchangeToken(code)
|
||||
|
||||
log.Debug().Str("token", token).Msg("Got token")
|
||||
|
||||
if handleApiError(c, "Failed to exchange token", tokenErr) {
|
||||
return
|
||||
}
|
||||
|
||||
email, emailErr := api.Providers.GetUser(providerName.Provider)
|
||||
|
||||
log.Debug().Str("email", email).Msg("Got email")
|
||||
|
||||
if handleApiError(c, "Failed to get user", emailErr) {
|
||||
return
|
||||
}
|
||||
@@ -306,7 +331,7 @@ func (api *API) SetupRoutes() {
|
||||
if !api.Auth.EmailWhitelisted(email) {
|
||||
log.Warn().Str("email", email).Msg("Email not whitelisted")
|
||||
unauthorizedQuery, unauthorizedQueryErr := query.Values(types.UnauthorizedQuery{
|
||||
Email: email,
|
||||
Username: email,
|
||||
})
|
||||
if handleApiError(c, "Failed to build query", unauthorizedQueryErr) {
|
||||
return
|
||||
@@ -314,9 +339,12 @@ func (api *API) SetupRoutes() {
|
||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/unauthorized?%s", api.Config.AppURL, unauthorizedQuery.Encode()))
|
||||
}
|
||||
|
||||
session := sessions.Default(c)
|
||||
session.Set("tinyauth_sid", fmt.Sprintf("%s:%s", providerName.Provider, token))
|
||||
session.Save()
|
||||
log.Debug().Msg("Email whitelisted")
|
||||
|
||||
api.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
||||
Username: email,
|
||||
Provider: providerName.Provider,
|
||||
})
|
||||
|
||||
redirectURI, redirectURIErr := c.Cookie("tinyauth_redirect_uri")
|
||||
|
||||
@@ -327,12 +355,16 @@ func (api *API) SetupRoutes() {
|
||||
})
|
||||
}
|
||||
|
||||
log.Debug().Str("redirectURI", redirectURI).Msg("Got redirect URI")
|
||||
|
||||
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", api.Domain, api.Config.CookieSecure, true)
|
||||
|
||||
redirectQuery, redirectQueryErr := query.Values(types.LoginQuery{
|
||||
RedirectURI: redirectURI,
|
||||
})
|
||||
|
||||
log.Debug().Interface("redirectQuery", redirectQuery).Msg("Got redirect query")
|
||||
|
||||
if handleApiError(c, "Failed to build query", redirectQueryErr) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
v1.0.0
|
||||
v2.0.1
|
||||
@@ -3,6 +3,9 @@ package auth
|
||||
import (
|
||||
"tinyauth/internal/types"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -18,9 +21,9 @@ type Auth struct {
|
||||
OAuthWhitelist []string
|
||||
}
|
||||
|
||||
func (auth *Auth) GetUser(email string) *types.User {
|
||||
func (auth *Auth) GetUser(username string) *types.User {
|
||||
for _, user := range auth.Users {
|
||||
if user.Email == email {
|
||||
if user.Username == username {
|
||||
return &user
|
||||
}
|
||||
}
|
||||
@@ -43,3 +46,49 @@ func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) {
|
||||
log.Debug().Msg("Creating session cookie")
|
||||
sessions := sessions.Default(c)
|
||||
log.Debug().Interface("data", data).Msg("Setting session cookie")
|
||||
sessions.Set("username", data.Username)
|
||||
sessions.Set("provider", data.Provider)
|
||||
sessions.Save()
|
||||
}
|
||||
|
||||
func (auth *Auth) DeleteSessionCookie(c *gin.Context) {
|
||||
log.Debug().Msg("Deleting session cookie")
|
||||
sessions := sessions.Default(c)
|
||||
sessions.Clear()
|
||||
sessions.Save()
|
||||
}
|
||||
|
||||
func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
|
||||
log.Debug().Msg("Getting session cookie")
|
||||
sessions := sessions.Default(c)
|
||||
|
||||
cookieUsername := sessions.Get("username")
|
||||
cookieProvider := sessions.Get("provider")
|
||||
|
||||
log.Debug().Interface("cookieUsername", cookieUsername).Msg("Got username")
|
||||
log.Debug().Interface("cookieProvider", cookieProvider).Msg("Got provider")
|
||||
|
||||
username, usernameOk := cookieUsername.(string)
|
||||
provider, providerOk := cookieProvider.(string)
|
||||
|
||||
log.Debug().Str("username", username).Bool("usernameOk", usernameOk).Str("provider", provider).Bool("providerOk", providerOk).Msg("Parsed cookie")
|
||||
|
||||
if !usernameOk || !providerOk {
|
||||
log.Warn().Msg("Session cookie invalid")
|
||||
return types.SessionCookie{}, nil
|
||||
}
|
||||
|
||||
return types.SessionCookie{
|
||||
Username: username,
|
||||
Provider: provider,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (auth *Auth) UserAuthConfigured() bool {
|
||||
return len(auth.Users) > 0
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"tinyauth/internal/auth"
|
||||
"tinyauth/internal/providers"
|
||||
"tinyauth/internal/types"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/oauth2"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func NewHooks(auth *auth.Auth, providers *providers.Providers) *Hooks {
|
||||
@@ -23,103 +21,62 @@ type Hooks struct {
|
||||
Providers *providers.Providers
|
||||
}
|
||||
|
||||
func (hooks *Hooks) UseUserContext(c *gin.Context) (types.UserContext, error) {
|
||||
session := sessions.Default(c)
|
||||
sessionCookie := session.Get("tinyauth_sid")
|
||||
func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
|
||||
cookie, cookiErr := hooks.Auth.GetSessionCookie(c)
|
||||
|
||||
if sessionCookie == nil {
|
||||
if cookiErr != nil {
|
||||
log.Error().Err(cookiErr).Msg("Failed to get session cookie")
|
||||
return types.UserContext{
|
||||
Email: "",
|
||||
Username: "",
|
||||
IsLoggedIn: false,
|
||||
OAuth: false,
|
||||
Provider: "",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
data, dataOk := sessionCookie.(string)
|
||||
log.Debug().Interface("cookie", cookie).Msg("Got session cookie")
|
||||
|
||||
if !dataOk {
|
||||
return types.UserContext{
|
||||
Email: "",
|
||||
IsLoggedIn: false,
|
||||
OAuth: false,
|
||||
Provider: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
split := strings.Split(data, ":")
|
||||
|
||||
if len(split) != 2 {
|
||||
return types.UserContext{
|
||||
Email: "",
|
||||
IsLoggedIn: false,
|
||||
OAuth: false,
|
||||
Provider: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
sessionType := split[0]
|
||||
sessionValue := split[1]
|
||||
|
||||
if sessionType == "email" {
|
||||
user := hooks.Auth.GetUser(sessionValue)
|
||||
if user == nil {
|
||||
if cookie.Provider == "username" {
|
||||
log.Debug().Msg("Provider is username")
|
||||
if hooks.Auth.GetUser(cookie.Username) != nil {
|
||||
log.Debug().Msg("User exists")
|
||||
return types.UserContext{
|
||||
Email: "",
|
||||
Username: cookie.Username,
|
||||
IsLoggedIn: true,
|
||||
OAuth: false,
|
||||
Provider: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().Msg("Provider is not username")
|
||||
provider := hooks.Providers.GetProvider(cookie.Provider)
|
||||
|
||||
if provider != nil {
|
||||
log.Debug().Msg("Provider exists")
|
||||
if !hooks.Auth.EmailWhitelisted(cookie.Username) {
|
||||
log.Error().Msgf("Email %s not whitelisted", cookie.Username)
|
||||
hooks.Auth.DeleteSessionCookie(c)
|
||||
return types.UserContext{
|
||||
Username: "",
|
||||
IsLoggedIn: false,
|
||||
OAuth: false,
|
||||
Provider: "",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
log.Debug().Msg("Email is whitelisted")
|
||||
return types.UserContext{
|
||||
Email: sessionValue,
|
||||
Username: cookie.Username,
|
||||
IsLoggedIn: true,
|
||||
OAuth: false,
|
||||
Provider: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
provider := hooks.Providers.GetProvider(sessionType)
|
||||
|
||||
if provider == nil {
|
||||
return types.UserContext{
|
||||
Email: "",
|
||||
IsLoggedIn: false,
|
||||
OAuth: false,
|
||||
Provider: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
provider.Token = &oauth2.Token{
|
||||
AccessToken: sessionValue,
|
||||
}
|
||||
|
||||
email, emailErr := hooks.Providers.GetUser(sessionType)
|
||||
|
||||
if emailErr != nil {
|
||||
return types.UserContext{
|
||||
Email: "",
|
||||
IsLoggedIn: false,
|
||||
OAuth: false,
|
||||
Provider: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if !hooks.Auth.EmailWhitelisted(email) {
|
||||
session.Delete("tinyauth_sid")
|
||||
session.Save()
|
||||
return types.UserContext{
|
||||
Email: "",
|
||||
IsLoggedIn: false,
|
||||
OAuth: false,
|
||||
Provider: "",
|
||||
}, nil
|
||||
OAuth: true,
|
||||
Provider: cookie.Provider,
|
||||
}
|
||||
}
|
||||
|
||||
return types.UserContext{
|
||||
Email: email,
|
||||
IsLoggedIn: true,
|
||||
OAuth: true,
|
||||
Provider: sessionType,
|
||||
}, nil
|
||||
Username: "",
|
||||
IsLoggedIn: false,
|
||||
OAuth: false,
|
||||
Provider: "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type GenericUserInfoResponse struct {
|
||||
@@ -17,12 +19,16 @@ func GetGenericEmail(client *http.Client, url string) (string, error) {
|
||||
return "", resErr
|
||||
}
|
||||
|
||||
log.Debug().Msg("Got response from generic provider")
|
||||
|
||||
body, bodyErr := io.ReadAll(res.Body)
|
||||
|
||||
if bodyErr != nil {
|
||||
return "", bodyErr
|
||||
}
|
||||
|
||||
log.Debug().Msg("Read body from generic provider")
|
||||
|
||||
var user GenericUserInfoResponse
|
||||
|
||||
jsonErr := json.Unmarshal(body, &user)
|
||||
@@ -31,5 +37,7 @@ func GetGenericEmail(client *http.Client, url string) (string, error) {
|
||||
return "", jsonErr
|
||||
}
|
||||
|
||||
log.Debug().Interface("user", user).Msg("Parsed user from generic provider")
|
||||
|
||||
return user.Email, nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type GithubUserInfoResponse []struct {
|
||||
@@ -23,12 +25,16 @@ func GetGithubEmail(client *http.Client) (string, error) {
|
||||
return "", resErr
|
||||
}
|
||||
|
||||
log.Debug().Msg("Got response from github")
|
||||
|
||||
body, bodyErr := io.ReadAll(res.Body)
|
||||
|
||||
if bodyErr != nil {
|
||||
return "", bodyErr
|
||||
}
|
||||
|
||||
log.Debug().Msg("Read body from github")
|
||||
|
||||
var emails GithubUserInfoResponse
|
||||
|
||||
jsonErr := json.Unmarshal(body, &emails)
|
||||
@@ -37,6 +43,8 @@ func GetGithubEmail(client *http.Client) (string, error) {
|
||||
return "", jsonErr
|
||||
}
|
||||
|
||||
log.Debug().Interface("emails", emails).Msg("Parsed emails from github")
|
||||
|
||||
for _, email := range emails {
|
||||
if email.Primary {
|
||||
return email.Email, nil
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type GoogleUserInfoResponse struct {
|
||||
@@ -21,12 +23,16 @@ func GetGoogleEmail(client *http.Client) (string, error) {
|
||||
return "", resErr
|
||||
}
|
||||
|
||||
log.Debug().Msg("Got response from google")
|
||||
|
||||
body, bodyErr := io.ReadAll(res.Body)
|
||||
|
||||
if bodyErr != nil {
|
||||
return "", bodyErr
|
||||
}
|
||||
|
||||
log.Debug().Msg("Read body from google")
|
||||
|
||||
var user GoogleUserInfoResponse
|
||||
|
||||
jsonErr := json.Unmarshal(body, &user)
|
||||
@@ -35,5 +41,7 @@ func GetGoogleEmail(client *http.Client) (string, error) {
|
||||
return "", jsonErr
|
||||
}
|
||||
|
||||
log.Debug().Interface("user", user).Msg("Parsed user from google")
|
||||
|
||||
return user.Email, nil
|
||||
}
|
||||
|
||||
@@ -79,33 +79,42 @@ func (providers *Providers) GetUser(provider string) (string, error) {
|
||||
switch provider {
|
||||
case "github":
|
||||
if providers.Github == nil {
|
||||
log.Debug().Msg("Github provider not configured")
|
||||
return "", nil
|
||||
}
|
||||
client := providers.Github.GetClient()
|
||||
log.Debug().Msg("Got client from github")
|
||||
email, emailErr := GetGithubEmail(client)
|
||||
if emailErr != nil {
|
||||
return "", emailErr
|
||||
}
|
||||
log.Debug().Msg("Got email from github")
|
||||
return email, nil
|
||||
case "google":
|
||||
if providers.Google == nil {
|
||||
log.Debug().Msg("Google provider not configured")
|
||||
return "", nil
|
||||
}
|
||||
client := providers.Google.GetClient()
|
||||
log.Debug().Msg("Got client from google")
|
||||
email, emailErr := GetGoogleEmail(client)
|
||||
if emailErr != nil {
|
||||
return "", emailErr
|
||||
}
|
||||
log.Debug().Msg("Got email from google")
|
||||
return email, nil
|
||||
case "generic":
|
||||
if providers.Generic == nil {
|
||||
log.Debug().Msg("Generic provider not configured")
|
||||
return "", nil
|
||||
}
|
||||
client := providers.Generic.GetClient()
|
||||
log.Debug().Msg("Got client from generic")
|
||||
email, emailErr := GetGenericEmail(client, providers.Config.GenericUserURL)
|
||||
if emailErr != nil {
|
||||
return "", emailErr
|
||||
}
|
||||
log.Debug().Msg("Got email from generic")
|
||||
return email, nil
|
||||
default:
|
||||
return "", nil
|
||||
|
||||
@@ -7,42 +7,47 @@ type LoginQuery struct {
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Email string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
type Users []User
|
||||
|
||||
type Config struct {
|
||||
Port int `validate:"number" mapstructure:"port"`
|
||||
Address string `mapstructure:"address, ip4_addr"`
|
||||
Secret string `validate:"required,len=32" mapstructure:"secret"`
|
||||
AppURL string `validate:"required,url" mapstructure:"app-url"`
|
||||
Users string `mapstructure:"users"`
|
||||
UsersFile string `mapstructure:"users-file"`
|
||||
CookieSecure bool `mapstructure:"cookie-secure"`
|
||||
GithubClientId string `mapstructure:"github-client-id"`
|
||||
GithubClientSecret string `mapstructure:"github-client-secret"`
|
||||
GoogleClientId string `mapstructure:"google-client-id"`
|
||||
GoogleClientSecret string `mapstructure:"google-client-secret"`
|
||||
GenericClientId string `mapstructure:"generic-client-id"`
|
||||
GenericClientSecret string `mapstructure:"generic-client-secret"`
|
||||
GenericScopes string `mapstructure:"generic-scopes"`
|
||||
GenericAuthURL string `mapstructure:"generic-auth-url"`
|
||||
GenericTokenURL string `mapstructure:"generic-token-url"`
|
||||
GenericUserURL string `mapstructure:"generic-user-info-url"`
|
||||
DisableContinue bool `mapstructure:"disable-continue"`
|
||||
OAuthWhitelist string `mapstructure:"oauth-whitelist"`
|
||||
CookieExpiry int `mapstructure:"cookie-expiry"`
|
||||
Port int `mapstructure:"port"`
|
||||
Address string `validate:"ip4_addr" mapstructure:"address"`
|
||||
Secret string `validate:"len=32" mapstructure:"secret"`
|
||||
SecretFile string `mapstructure:"secret-file"`
|
||||
AppURL string `validate:"required,url" mapstructure:"app-url"`
|
||||
Users string `mapstructure:"users"`
|
||||
UsersFile string `mapstructure:"users-file"`
|
||||
CookieSecure bool `mapstructure:"cookie-secure"`
|
||||
GithubClientId string `mapstructure:"github-client-id"`
|
||||
GithubClientSecret string `mapstructure:"github-client-secret"`
|
||||
GithubClientSecretFile string `mapstructure:"github-client-secret-file"`
|
||||
GoogleClientId string `mapstructure:"google-client-id"`
|
||||
GoogleClientSecret string `mapstructure:"google-client-secret"`
|
||||
GoogleClientSecretFile string `mapstructure:"google-client-secret-file"`
|
||||
GenericClientId string `mapstructure:"generic-client-id"`
|
||||
GenericClientSecret string `mapstructure:"generic-client-secret"`
|
||||
GenericClientSecretFile string `mapstructure:"generic-client-secret-file"`
|
||||
GenericScopes string `mapstructure:"generic-scopes"`
|
||||
GenericAuthURL string `mapstructure:"generic-auth-url"`
|
||||
GenericTokenURL string `mapstructure:"generic-token-url"`
|
||||
GenericUserURL string `mapstructure:"generic-user-info-url"`
|
||||
DisableContinue bool `mapstructure:"disable-continue"`
|
||||
OAuthWhitelist string `mapstructure:"oauth-whitelist"`
|
||||
CookieExpiry int `mapstructure:"cookie-expiry"`
|
||||
LogLevel int8 `mapstructure:"log-level"`
|
||||
}
|
||||
|
||||
type UserContext struct {
|
||||
Email string
|
||||
Username string
|
||||
IsLoggedIn bool
|
||||
OAuth bool
|
||||
Provider string
|
||||
@@ -83,5 +88,10 @@ type OAuthProviders struct {
|
||||
}
|
||||
|
||||
type UnauthorizedQuery struct {
|
||||
Email string `url:"email"`
|
||||
Username string `url:"username"`
|
||||
}
|
||||
|
||||
type SessionCookie struct {
|
||||
Username string
|
||||
Provider string
|
||||
}
|
||||
|
||||
@@ -6,27 +6,35 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"tinyauth/internal/types"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func ParseUsers(users string) (types.Users, error) {
|
||||
log.Debug().Msg("Parsing users")
|
||||
var usersParsed types.Users
|
||||
userList := strings.Split(users, ",")
|
||||
|
||||
log.Debug().Strs("users", userList).Msg("Splitted users")
|
||||
|
||||
if len(userList) == 0 {
|
||||
return types.Users{}, errors.New("invalid user format")
|
||||
}
|
||||
|
||||
for _, user := range userList {
|
||||
userSplit := strings.Split(user, ":")
|
||||
log.Debug().Strs("user", userSplit).Msg("Splitting user")
|
||||
if len(userSplit) != 2 {
|
||||
return types.Users{}, errors.New("invalid user format")
|
||||
}
|
||||
usersParsed = append(usersParsed, types.User{
|
||||
Email: userSplit[0],
|
||||
Username: userSplit[0],
|
||||
Password: userSplit[1],
|
||||
})
|
||||
}
|
||||
|
||||
log.Debug().Interface("users", usersParsed).Msg("Parsed users")
|
||||
|
||||
return usersParsed, nil
|
||||
}
|
||||
|
||||
@@ -44,14 +52,14 @@ func GetRootURL(urlSrc string) (string, error) {
|
||||
return urlFinal, nil
|
||||
}
|
||||
|
||||
func GetUsersFromFile(usersFile string) (string, error) {
|
||||
_, statErr := os.Stat(usersFile)
|
||||
func ReadFile(file string) (string, error) {
|
||||
_, statErr := os.Stat(file)
|
||||
|
||||
if statErr != nil {
|
||||
return "", statErr
|
||||
}
|
||||
|
||||
data, readErr := os.ReadFile(usersFile)
|
||||
data, readErr := os.ReadFile(file)
|
||||
|
||||
if readErr != nil {
|
||||
return "", readErr
|
||||
@@ -69,15 +77,61 @@ func ParseFileToLine(content string) string {
|
||||
continue
|
||||
}
|
||||
|
||||
users = append(users, line)
|
||||
users = append(users, strings.TrimSpace(line))
|
||||
}
|
||||
|
||||
return strings.Join(users, ",")
|
||||
}
|
||||
|
||||
func ParseCommaString(str string) []string {
|
||||
if str == "" {
|
||||
return []string{}
|
||||
func GetSecret(env string, file string) string {
|
||||
if env == "" && file == "" {
|
||||
log.Debug().Msg("No secret provided")
|
||||
return ""
|
||||
}
|
||||
return strings.Split(str, ",")
|
||||
|
||||
if env != "" {
|
||||
log.Debug().Str("secret", env).Msg("Using secret from env")
|
||||
return env
|
||||
}
|
||||
|
||||
contents, err := ReadFile(file)
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
log.Debug().Str("secret", contents).Msg("Using secret from file")
|
||||
|
||||
return contents
|
||||
}
|
||||
|
||||
func GetUsers(env string, file string) (types.Users, error) {
|
||||
var users string
|
||||
|
||||
if env == "" && file == "" {
|
||||
return types.Users{}, errors.New("no users provided")
|
||||
}
|
||||
|
||||
if env != "" {
|
||||
log.Debug().Str("users", env).Msg("Using users from env")
|
||||
users += env
|
||||
}
|
||||
|
||||
if file != "" {
|
||||
fileContents, fileErr := ReadFile(file)
|
||||
|
||||
if fileErr == nil {
|
||||
log.Debug().Str("users", ParseFileToLine(fileContents)).Msg("Using users from file")
|
||||
if users != "" {
|
||||
users += ","
|
||||
}
|
||||
users += ParseFileToLine(fileContents)
|
||||
}
|
||||
}
|
||||
|
||||
return ParseUsers(users)
|
||||
}
|
||||
|
||||
func OAuthConfigured(config types.Config) bool {
|
||||
return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "")
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export const LoginPage = () => {
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email(),
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ export const LoginPage = () => {
|
||||
const form = useForm({
|
||||
mode: "uncontrolled",
|
||||
initialValues: {
|
||||
email: "",
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
validate: zodResolver(schema),
|
||||
@@ -54,7 +54,7 @@ export const LoginPage = () => {
|
||||
onError: () => {
|
||||
notifications.show({
|
||||
title: "Failed to login",
|
||||
message: "Check your email and password",
|
||||
message: "Check your username and password",
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
@@ -153,40 +153,44 @@ export const LoginPage = () => {
|
||||
</Grid.Col>
|
||||
)}
|
||||
</Grid>
|
||||
<Divider
|
||||
label="Or continue with email"
|
||||
labelPosition="center"
|
||||
my="lg"
|
||||
/>
|
||||
{configuredProviders.includes("username") && (
|
||||
<Divider
|
||||
label="Or continue with password"
|
||||
labelPosition="center"
|
||||
my="lg"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<TextInput
|
||||
label="Email"
|
||||
placeholder="user@example.com"
|
||||
required
|
||||
disabled={loginMutation.isLoading}
|
||||
key={form.key("email")}
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="password"
|
||||
required
|
||||
mt="md"
|
||||
disabled={loginMutation.isLoading}
|
||||
key={form.key("password")}
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<Button
|
||||
fullWidth
|
||||
mt="xl"
|
||||
type="submit"
|
||||
loading={loginMutation.isLoading}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</form>
|
||||
{configuredProviders.includes("username") && (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<TextInput
|
||||
label="Username"
|
||||
placeholder="user@example.com"
|
||||
required
|
||||
disabled={loginMutation.isLoading}
|
||||
key={form.key("username")}
|
||||
{...form.getInputProps("username")}
|
||||
/>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="password"
|
||||
required
|
||||
mt="md"
|
||||
disabled={loginMutation.isLoading}
|
||||
key={form.key("password")}
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<Button
|
||||
fullWidth
|
||||
mt="xl"
|
||||
type="submit"
|
||||
loading={loginMutation.isLoading}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</Paper>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Layout } from "../components/layouts/layout";
|
||||
import { capitalize } from "../utils/utils";
|
||||
|
||||
export const LogoutPage = () => {
|
||||
const { isLoggedIn, email, oauth, provider } = useUserContext();
|
||||
const { isLoggedIn, username, oauth, provider } = useUserContext();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <Navigate to="/login" />;
|
||||
@@ -44,7 +44,7 @@ export const LogoutPage = () => {
|
||||
Logout
|
||||
</Text>
|
||||
<Text>
|
||||
You are currently logged in as <Code>{email}</Code>
|
||||
You are currently logged in as <Code>{username}</Code>
|
||||
{oauth && ` using ${capitalize(provider)}`}. Click the button below to
|
||||
log out.
|
||||
</Text>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Navigate } from "react-router";
|
||||
export const UnauthorizedPage = () => {
|
||||
const queryString = window.location.search;
|
||||
const params = new URLSearchParams(queryString);
|
||||
const email = params.get("email");
|
||||
const username = params.get("username");
|
||||
|
||||
const { isLoggedIn } = useUserContext();
|
||||
|
||||
@@ -14,7 +14,7 @@ export const UnauthorizedPage = () => {
|
||||
return <Navigate to="/" />;
|
||||
}
|
||||
|
||||
if (email === "null") {
|
||||
if (username === "null") {
|
||||
return <Navigate to="/" />;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export const UnauthorizedPage = () => {
|
||||
Unauthorized
|
||||
</Text>
|
||||
<Text>
|
||||
The user with email address <Code>{email}</Code> is not authorized to
|
||||
The user with username <Code>{username}</Code> is not authorized to
|
||||
login.
|
||||
</Text>
|
||||
<Button
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
||||
|
||||
export const userContextSchema = z.object({
|
||||
isLoggedIn: z.boolean(),
|
||||
email: z.string(),
|
||||
username: z.string(),
|
||||
oauth: z.boolean(),
|
||||
provider: z.string(),
|
||||
configuredProviders: z.array(z.string()),
|
||||
|
||||
Reference in New Issue
Block a user