mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-11-03 23:55:44 +00:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
			v1.0.0-alp
			...
			v1.0.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					61fffb9708 | ||
| 
						 | 
					9d2aef163b | ||
| 
						 | 
					cc480085c5 | ||
| 
						 | 
					2c7144937a | ||
| 
						 | 
					c7ec788ce1 | ||
| 
						 | 
					96a373a794 | ||
| 
						 | 
					c5a8639822 | ||
| 
						 | 
					b87cb54d91 | 
@@ -35,7 +35,7 @@ COPY ./cmd ./cmd
 | 
			
		||||
COPY ./internal ./internal
 | 
			
		||||
COPY --from=site-builder /site/dist ./internal/assets/dist
 | 
			
		||||
 | 
			
		||||
RUN go build
 | 
			
		||||
RUN CGO_ENABLED=0 go build
 | 
			
		||||
 | 
			
		||||
# Runner
 | 
			
		||||
FROM alpine:3.21 AS runner
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										50
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								README.md
									
									
									
									
									
								
							@@ -1,36 +1,46 @@
 | 
			
		||||
# Tinyauth - The simplest way to protect your apps with a login screen
 | 
			
		||||
<div align="center">
 | 
			
		||||
    <img alt="Tinyauth" title="Tinyauth" width="256" src="site/public/logo.png">
 | 
			
		||||
    <h1>Tinyauth</h1>
 | 
			
		||||
    <p>The easiest way to secure your apps with a login screen.</p>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
Tinyauth is an extremely simple traefik middleware that adds a login screen to all of your apps that are using the traefik reverse proxy. Tinyauth is configurable through environment variables and it is only 20MB in size.
 | 
			
		||||
<div align="center">
 | 
			
		||||
    <img alt="License" src="https://img.shields.io/github/license/steveiliop56/tinyauth">
 | 
			
		||||
    <img alt="Release" src="https://img.shields.io/github/v/release/steveiliop56/tinyauth">
 | 
			
		||||
    <img alt="Commit activity" src="https://img.shields.io/github/commit-activity/w/steveiliop56/tinyauth">
 | 
			
		||||
    <img alt="Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/steveiliop56/tinyauth/release.yml">
 | 
			
		||||
    <img alt="Issues" src="https://img.shields.io/github/issues/steveiliop56/tinyauth">
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
## Getting started
 | 
			
		||||
<br />
 | 
			
		||||
 | 
			
		||||
Tinyauth is extremely easy to run since it's shipped as a docker container. The guide on how to get started is available on the website [here](https://tinyauth.doesmycode.work/).
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
## FAQ
 | 
			
		||||
> [!WARNING]
 | 
			
		||||
> Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating.
 | 
			
		||||
 | 
			
		||||
### Why?
 | 
			
		||||
> [!NOTE]
 | 
			
		||||
> Tinyauth is intended for homelab use and it is not made for production use cases. If you are looking for something production ready please use [authentik](https://goauthentik.io).
 | 
			
		||||
 | 
			
		||||
Why make this project? Well, we all know that more powerful alternatives like authentik and authelia exist, but when I tried to use them, I felt overwhelmed with all the configration options and environment variables I had to configure in order for them to work. So, I decided to make a small alternative in Go to both test my skills and cover my simple login screen needs.
 | 
			
		||||
## Getting Started
 | 
			
		||||
 | 
			
		||||
### Is this secure?
 | 
			
		||||
You can easily get started with tinyauth by following the guide on the documentation [here](https://tinyauth.doesmycode.work/docs/getting-started.html). There is also an available docker compose file [here](./docker-compose.example.yml) that has traefik, nginx and tinyauth to demonstrate its capabilities.
 | 
			
		||||
 | 
			
		||||
Probably, the sessions are managed with the gin sessions package so it should be very secure. It is definitely not made for production but it could easily serve as a simple login screen to all of your homelab apps.
 | 
			
		||||
## Documentation
 | 
			
		||||
 | 
			
		||||
### Do I need to login every time?
 | 
			
		||||
 | 
			
		||||
No, when you login, tinyauth sets a `tinyauth` cookie in your browser that applies to all of the subdomains of your domain.
 | 
			
		||||
 | 
			
		||||
## License
 | 
			
		||||
 | 
			
		||||
Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions.
 | 
			
		||||
You can find documentation and guides on all available configuration of tinyauth [here](https://tinyauth.doesmycode.work).
 | 
			
		||||
 | 
			
		||||
## Contributing
 | 
			
		||||
 | 
			
		||||
Any contributions to the codebase are welcome! I am not a cybersecurity person so my code may have a security issue, if you find something that could be used to exploit and bypass tinyauth please let me know as soon as possible so I can fix it.
 | 
			
		||||
All contributions to the codebase are welcome! If you have any recommendations on how to improve security or find a security issue in tinyauth please open an issue or pull request so it can be fixed as soon as possible!
 | 
			
		||||
 | 
			
		||||
## License
 | 
			
		||||
 | 
			
		||||
Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions. For more information about the license check the [license](./LICENSE) file.
 | 
			
		||||
 | 
			
		||||
## Acknowledgements
 | 
			
		||||
 | 
			
		||||
Credits for the logo go to:
 | 
			
		||||
Credits for the logo of this app go to:
 | 
			
		||||
 | 
			
		||||
- Freepik for providing the hat and police badge.
 | 
			
		||||
- Renee French for making the gopher logo.
 | 
			
		||||
- **Freepik** for providing the police hat and logo.
 | 
			
		||||
- **Renee French** for the original gopher logo.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								cmd/root.go
									
									
									
									
									
								
							@@ -56,6 +56,9 @@ var rootCmd = &cobra.Command{
 | 
			
		||||
		users, parseErr := utils.ParseUsers(usersString)
 | 
			
		||||
		HandleError(parseErr, "Failed to parse users")
 | 
			
		||||
 | 
			
		||||
		// Create oauth whitelist
 | 
			
		||||
		oauthWhitelist := utils.ParseCommaString(config.OAuthWhitelist)
 | 
			
		||||
 | 
			
		||||
		// Create OAuth config
 | 
			
		||||
		oauthConfig := types.OAuthConfig{
 | 
			
		||||
			GithubClientId:      config.GithubClientId,
 | 
			
		||||
@@ -64,15 +67,15 @@ var rootCmd = &cobra.Command{
 | 
			
		||||
			GoogleClientSecret:  config.GoogleClientSecret,
 | 
			
		||||
			GenericClientId:     config.GenericClientId,
 | 
			
		||||
			GenericClientSecret: config.GenericClientSecret,
 | 
			
		||||
			GenericScopes:       config.GenericScopes,
 | 
			
		||||
			GenericScopes:       utils.ParseCommaString(config.GenericScopes),
 | 
			
		||||
			GenericAuthURL:      config.GenericAuthURL,
 | 
			
		||||
			GenericTokenURL:     config.GenericTokenURL,
 | 
			
		||||
			GenericUserInfoURL:  config.GenericUserInfoURL,
 | 
			
		||||
			GenericUserURL:      config.GenericUserURL,
 | 
			
		||||
			AppURL:              config.AppURL,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Create auth service
 | 
			
		||||
		auth := auth.NewAuth(users)
 | 
			
		||||
		auth := auth.NewAuth(users, oauthWhitelist)
 | 
			
		||||
 | 
			
		||||
		// Create OAuth providers service
 | 
			
		||||
		providers := providers.NewProviders(oauthConfig)
 | 
			
		||||
@@ -134,8 +137,10 @@ func init() {
 | 
			
		||||
	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.")
 | 
			
		||||
	rootCmd.Flags().String("generic-user-info-url", "", "Generic OAuth user info URL.")
 | 
			
		||||
	rootCmd.Flags().String("generic-user-url", "", "Generic OAuth user info URL.")
 | 
			
		||||
	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.")
 | 
			
		||||
	viper.BindEnv("port", "PORT")
 | 
			
		||||
	viper.BindEnv("address", "ADDRESS")
 | 
			
		||||
	viper.BindEnv("secret", "SECRET")
 | 
			
		||||
@@ -152,7 +157,9 @@ func init() {
 | 
			
		||||
	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-info-url", "GENERIC_USER_INFO_URL")
 | 
			
		||||
	viper.BindEnv("generic-user-url", "GENERIC_USER_URL")
 | 
			
		||||
	viper.BindEnv("disable-continue", "DISABLE_CONTINUE")
 | 
			
		||||
	viper.BindEnv("oauth-whitelist", "WHITELIST")
 | 
			
		||||
	viper.BindEnv("cookie-expiry", "COOKIE_EXPIRY")
 | 
			
		||||
	viper.BindPFlags(rootCmd.Flags())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -77,6 +77,7 @@ func (api *API) Init() {
 | 
			
		||||
		Path:     "/",
 | 
			
		||||
		HttpOnly: true,
 | 
			
		||||
		Secure:   isSecure,
 | 
			
		||||
		MaxAge:   api.Config.CookieExpiry,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	router.Use(sessions.Sessions("tinyauth", store))
 | 
			
		||||
@@ -279,46 +280,48 @@ func (api *API) SetupRoutes() {
 | 
			
		||||
 | 
			
		||||
		bindErr := c.BindUri(&providerName)
 | 
			
		||||
 | 
			
		||||
		if bindErr != nil {
 | 
			
		||||
			log.Error().Err(bindErr).Msg("Failed to bind URI")
 | 
			
		||||
			c.JSON(400, gin.H{
 | 
			
		||||
				"status":  400,
 | 
			
		||||
				"message": "Bad Request",
 | 
			
		||||
			})
 | 
			
		||||
		if handleApiError(c, "Failed to bind URI", bindErr) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		code := c.Query("code")
 | 
			
		||||
 | 
			
		||||
		if code == "" {
 | 
			
		||||
			c.JSON(400, gin.H{
 | 
			
		||||
				"status":  400,
 | 
			
		||||
				"message": "Bad Request",
 | 
			
		||||
			})
 | 
			
		||||
			log.Error().Msg("No code provided")
 | 
			
		||||
			c.Redirect(http.StatusPermanentRedirect, "/error")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		provider := api.Providers.GetProvider(providerName.Provider)
 | 
			
		||||
 | 
			
		||||
		if provider == nil {
 | 
			
		||||
			c.JSON(404, gin.H{
 | 
			
		||||
				"status":  404,
 | 
			
		||||
				"message": "Not Found",
 | 
			
		||||
			})
 | 
			
		||||
			c.Redirect(http.StatusPermanentRedirect, "/not-found")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		token, tokenErr := provider.ExchangeToken(code)
 | 
			
		||||
 | 
			
		||||
		if tokenErr != nil {
 | 
			
		||||
			log.Error().Err(tokenErr).Msg("Failed to exchange token")
 | 
			
		||||
			c.JSON(500, gin.H{
 | 
			
		||||
				"status":  500,
 | 
			
		||||
				"message": "Internal Server Error",
 | 
			
		||||
			})
 | 
			
		||||
		if handleApiError(c, "Failed to exchange token", tokenErr) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		email, emailErr := api.Providers.GetUser(providerName.Provider)
 | 
			
		||||
 | 
			
		||||
		if handleApiError(c, "Failed to get user", emailErr) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !api.Auth.EmailWhitelisted(email) {
 | 
			
		||||
			log.Warn().Str("email", email).Msg("Email not whitelisted")
 | 
			
		||||
			unauthorizedQuery, unauthorizedQueryErr := query.Values(types.UnauthorizedQuery{
 | 
			
		||||
				Email: email,
 | 
			
		||||
			})
 | 
			
		||||
			if handleApiError(c, "Failed to build query", unauthorizedQueryErr) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			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()
 | 
			
		||||
@@ -334,20 +337,15 @@ func (api *API) SetupRoutes() {
 | 
			
		||||
 | 
			
		||||
		c.SetCookie("tinyauth_redirect_uri", "", -1, "/", api.Domain, api.Config.CookieSecure, true)
 | 
			
		||||
 | 
			
		||||
		queries, queryErr := query.Values(types.LoginQuery{
 | 
			
		||||
		redirectQuery, redirectQueryErr := query.Values(types.LoginQuery{
 | 
			
		||||
			RedirectURI: redirectURI,
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		if queryErr != nil {
 | 
			
		||||
			log.Error().Err(queryErr).Msg("Failed to build query")
 | 
			
		||||
			c.JSON(501, gin.H{
 | 
			
		||||
				"status":  501,
 | 
			
		||||
				"message": "Internal Server Error",
 | 
			
		||||
			})
 | 
			
		||||
		if handleApiError(c, "Failed to build query", redirectQueryErr) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/continue?%s", api.Config.AppURL, queries.Encode()))
 | 
			
		||||
		c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/continue?%s", api.Config.AppURL, redirectQuery.Encode()))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -379,3 +377,12 @@ func zerolog() gin.HandlerFunc {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func handleApiError(c *gin.Context, msg string, err error) bool {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error().Err(err).Msg(msg)
 | 
			
		||||
		c.Redirect(http.StatusPermanentRedirect, "/error")
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,14 +6,16 @@ import (
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func NewAuth(userList types.Users) *Auth {
 | 
			
		||||
func NewAuth(userList types.Users, oauthWhitelist []string) *Auth {
 | 
			
		||||
	return &Auth{
 | 
			
		||||
		Users: userList,
 | 
			
		||||
		Users:          userList,
 | 
			
		||||
		OAuthWhitelist: oauthWhitelist,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Auth struct {
 | 
			
		||||
	Users types.Users
 | 
			
		||||
	Users          types.Users
 | 
			
		||||
	OAuthWhitelist []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *Auth) GetUser(email string) *types.User {
 | 
			
		||||
@@ -28,4 +30,16 @@ func (auth *Auth) GetUser(email string) *types.User {
 | 
			
		||||
func (auth *Auth) CheckPassword(user types.User, password string) bool {
 | 
			
		||||
	hashedPasswordErr := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
 | 
			
		||||
	return hashedPasswordErr == nil
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
 | 
			
		||||
	if len(auth.OAuthWhitelist) == 0 {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	for _, email := range auth.OAuthWhitelist {
 | 
			
		||||
		if email == emailSrc {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -105,6 +105,17 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) (types.UserContext, error) {
 | 
			
		||||
		}, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !hooks.Auth.EmailWhitelisted(email) {
 | 
			
		||||
		session.Delete("tinyauth_sid")
 | 
			
		||||
		session.Save()
 | 
			
		||||
		return types.UserContext{
 | 
			
		||||
			Email:      "",
 | 
			
		||||
			IsLoggedIn: false,
 | 
			
		||||
			OAuth:      false,
 | 
			
		||||
			Provider:   "",
 | 
			
		||||
		}, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return types.UserContext{
 | 
			
		||||
		Email:      email,
 | 
			
		||||
		IsLoggedIn: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,7 @@ func (providers *Providers) Init() {
 | 
			
		||||
			ClientID:     providers.Config.GenericClientId,
 | 
			
		||||
			ClientSecret: providers.Config.GenericClientSecret,
 | 
			
		||||
			RedirectURL:  fmt.Sprintf("%s/api/oauth/callback/generic", providers.Config.AppURL),
 | 
			
		||||
			Scopes:       []string{providers.Config.GenericScopes},
 | 
			
		||||
			Scopes:       providers.Config.GenericScopes,
 | 
			
		||||
			Endpoint: oauth2.Endpoint{
 | 
			
		||||
				AuthURL:  providers.Config.GenericAuthURL,
 | 
			
		||||
				TokenURL: providers.Config.GenericTokenURL,
 | 
			
		||||
@@ -102,7 +102,7 @@ func (providers *Providers) GetUser(provider string) (string, error) {
 | 
			
		||||
			return "", nil
 | 
			
		||||
		}
 | 
			
		||||
		client := providers.Generic.GetClient()
 | 
			
		||||
		email, emailErr := GetGenericEmail(client, providers.Config.GenericUserInfoURL)
 | 
			
		||||
		email, emailErr := GetGenericEmail(client, providers.Config.GenericUserURL)
 | 
			
		||||
		if emailErr != nil {
 | 
			
		||||
			return "", emailErr
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -35,8 +35,10 @@ type Config struct {
 | 
			
		||||
	GenericScopes       string `mapstructure:"generic-scopes"`
 | 
			
		||||
	GenericAuthURL      string `mapstructure:"generic-auth-url"`
 | 
			
		||||
	GenericTokenURL     string `mapstructure:"generic-token-url"`
 | 
			
		||||
	GenericUserInfoURL  string `mapstructure:"generic-user-info-url"`
 | 
			
		||||
	GenericUserURL      string `mapstructure:"generic-user-info-url"`
 | 
			
		||||
	DisableContinue     bool   `mapstructure:"disable-continue"`
 | 
			
		||||
	OAuthWhitelist      string `mapstructure:"oauth-whitelist"`
 | 
			
		||||
	CookieExpiry        int    `mapstructure:"cookie-expiry"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UserContext struct {
 | 
			
		||||
@@ -52,6 +54,7 @@ type APIConfig struct {
 | 
			
		||||
	Secret          string
 | 
			
		||||
	AppURL          string
 | 
			
		||||
	CookieSecure    bool
 | 
			
		||||
	CookieExpiry    int
 | 
			
		||||
	DisableContinue bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -62,10 +65,10 @@ type OAuthConfig struct {
 | 
			
		||||
	GoogleClientSecret  string
 | 
			
		||||
	GenericClientId     string
 | 
			
		||||
	GenericClientSecret string
 | 
			
		||||
	GenericScopes       string
 | 
			
		||||
	GenericScopes       []string
 | 
			
		||||
	GenericAuthURL      string
 | 
			
		||||
	GenericTokenURL     string
 | 
			
		||||
	GenericUserInfoURL  string
 | 
			
		||||
	GenericUserURL      string
 | 
			
		||||
	AppURL              string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -78,3 +81,7 @@ type OAuthProviders struct {
 | 
			
		||||
	Google    *oauth.OAuth
 | 
			
		||||
	Microsoft *oauth.OAuth
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UnauthorizedQuery struct {
 | 
			
		||||
	Email string `url:"email"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -74,3 +74,10 @@ func ParseFileToLine(content string) string {
 | 
			
		||||
 | 
			
		||||
	return strings.Join(users, ",")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParseCommaString(str string) []string {
 | 
			
		||||
	if str == "" {
 | 
			
		||||
		return []string{}
 | 
			
		||||
	}
 | 
			
		||||
	return strings.Split(str, ",")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,8 @@ import { LoginPage } from "./pages/login-page.tsx";
 | 
			
		||||
import { LogoutPage } from "./pages/logout-page.tsx";
 | 
			
		||||
import { ContinuePage } from "./pages/continue-page.tsx";
 | 
			
		||||
import { NotFoundPage } from "./pages/not-found-page.tsx";
 | 
			
		||||
import { UnauthorizedPage } from "./pages/unauthorized-page.tsx";
 | 
			
		||||
import { InternalServerError } from "./pages/internal-server-error.tsx";
 | 
			
		||||
 | 
			
		||||
const queryClient = new QueryClient({
 | 
			
		||||
  defaultOptions: {
 | 
			
		||||
@@ -34,6 +36,8 @@ createRoot(document.getElementById("root")!).render(
 | 
			
		||||
              <Route path="/login" element={<LoginPage />} />
 | 
			
		||||
              <Route path="/logout" element={<LogoutPage />} />
 | 
			
		||||
              <Route path="/continue" element={<ContinuePage />} />
 | 
			
		||||
              <Route path="/unauthorized" element={<UnauthorizedPage />} />
 | 
			
		||||
              <Route path="/error" element={<InternalServerError />} />
 | 
			
		||||
              <Route path="*" element={<NotFoundPage />} />
 | 
			
		||||
            </Routes>
 | 
			
		||||
          </BrowserRouter>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								site/src/pages/internal-server-error.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								site/src/pages/internal-server-error.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
import { Button, Paper, Text } from "@mantine/core";
 | 
			
		||||
import { Layout } from "../components/layouts/layout";
 | 
			
		||||
 | 
			
		||||
export const InternalServerError = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Paper shadow="md" p={30} mt={30} radius="md" withBorder>
 | 
			
		||||
        <Text size="xl" fw={700}>
 | 
			
		||||
          Internal Server Error
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Text>
 | 
			
		||||
          An error occured on the server and it currently cannot serve your
 | 
			
		||||
          request.
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Button fullWidth mt="xl" onClick={() => window.location.replace("/")}>
 | 
			
		||||
          Try again
 | 
			
		||||
        </Button>
 | 
			
		||||
      </Paper>
 | 
			
		||||
    </Layout>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										41
									
								
								site/src/pages/unauthorized-page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								site/src/pages/unauthorized-page.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
import { Button, Code, Paper, Text } from "@mantine/core";
 | 
			
		||||
import { Layout } from "../components/layouts/layout";
 | 
			
		||||
import { useUserContext } from "../context/user-context";
 | 
			
		||||
import { Navigate } from "react-router";
 | 
			
		||||
 | 
			
		||||
export const UnauthorizedPage = () => {
 | 
			
		||||
  const queryString = window.location.search;
 | 
			
		||||
  const params = new URLSearchParams(queryString);
 | 
			
		||||
  const email = params.get("email");
 | 
			
		||||
 | 
			
		||||
  const { isLoggedIn } = useUserContext();
 | 
			
		||||
 | 
			
		||||
  if (isLoggedIn) {
 | 
			
		||||
    return <Navigate to="/" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (email === "null") {
 | 
			
		||||
    return <Navigate to="/" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Paper shadow="md" p={30} mt={30} radius="md" withBorder>
 | 
			
		||||
        <Text size="xl" fw={700}>
 | 
			
		||||
          Unauthorized
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Text>
 | 
			
		||||
          The user with email address <Code>{email}</Code> is not authorized to
 | 
			
		||||
          login.
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Button
 | 
			
		||||
          fullWidth
 | 
			
		||||
          mt="xl"
 | 
			
		||||
          onClick={() => window.location.replace("/login")}
 | 
			
		||||
        >
 | 
			
		||||
          Try again
 | 
			
		||||
        </Button>
 | 
			
		||||
      </Paper>
 | 
			
		||||
    </Layout>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
		Reference in New Issue
	
	Block a user