mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-31 06:05:43 +00:00 
			
		
		
		
	Compare commits
	
		
			31 Commits
		
	
	
		
			v0.2.0-bet
			...
			v1.0.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 2c7a3fc801 | ||
|   | 61fffb9708 | ||
|   | 9d2aef163b | ||
|   | cc480085c5 | ||
|   | 2c7144937a | ||
|   | c7ec788ce1 | ||
|   | 96a373a794 | ||
|   | c5a8639822 | ||
|   | b87cb54d91 | ||
|   | f61b6dbad4 | ||
|   | 35854f5ce4 | ||
|   | c59aaa5600 | ||
|   | 085b1492cc | ||
|   | a19f3589f8 | ||
|   | e88ec22ce3 | ||
|   | 90f4c3c980 | ||
|   | f487e25ac5 | ||
|   | d4eca52b12 | ||
|   | 433e71bd50 | ||
|   | 80d25551e0 | ||
|   | 143b13af2c | ||
|   | 4457d6f525 | ||
|   | b901744e03 | ||
|   | 61a7400cf1 | ||
|   | 40ab77cdd5 | ||
|   | 403787e56c | ||
|   | d3e52c925d | ||
|   | a4c717ba34 | ||
|   | 5e73d06fcc | ||
|   | 2988b5f22f | ||
|   | a28e55ae4c | 
| @@ -35,10 +35,10 @@ 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 busybox:1.37-musl AS runner | ||||
| FROM alpine:3.21 AS runner | ||||
|  | ||||
| WORKDIR /tinyauth | ||||
|  | ||||
| @@ -46,4 +46,4 @@ COPY --from=builder /tinyauth/tinyauth ./ | ||||
|  | ||||
| EXPOSE 3000 | ||||
|  | ||||
| CMD ["./tinyauth"] | ||||
| ENTRYPOINT ["./tinyauth"] | ||||
							
								
								
									
										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. | ||||
|   | ||||
							
								
								
									
										113
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								cmd/root.go
									
									
									
									
									
								
							| @@ -1,16 +1,15 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 	cmd "tinyauth/cmd/user" | ||||
| 	"tinyauth/internal/api" | ||||
| 	"tinyauth/internal/assets" | ||||
| 	"tinyauth/internal/auth" | ||||
| 	"tinyauth/internal/hooks" | ||||
| 	"tinyauth/internal/providers" | ||||
| 	"tinyauth/internal/types" | ||||
| 	"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" | ||||
| @@ -19,12 +18,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.`, | ||||
| 	Long:  `Tinyauth is an extremely simple traefik forward-auth login screen that makes securing your apps easy.`, | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 		// Logger | ||||
| 		log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger() | ||||
| 		log.Info().Str("version", assets.Version).Msg("Starting tinyauth") | ||||
|  | ||||
| 		// Get config | ||||
| 		log.Info().Msg("Parsing config") | ||||
| 		var config types.Config | ||||
| @@ -42,59 +37,129 @@ var rootCmd = &cobra.Command{ | ||||
|  | ||||
| 		if config.UsersFile == "" && config.Users == "" { | ||||
| 			log.Fatal().Msg("No users provided") | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
|  | ||||
| 		users := config.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 := strings.Join(strings.Split(usersFromFile, "\n"), ",") | ||||
| 			if users != "" { | ||||
| 				users = users + "," + usersFromFileParsed | ||||
| 			usersFromFileParsed := utils.ParseFileToLine(usersFromFile) | ||||
| 			if usersString != "" { | ||||
| 				usersString = usersString + "," + usersFromFileParsed | ||||
| 			} else { | ||||
| 				users = usersFromFileParsed | ||||
| 				usersString = usersFromFileParsed | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		userList, createErr := utils.ParseUsers(users) | ||||
| 		HandleError(createErr, "Failed to parse users") | ||||
| 		users, parseErr := utils.ParseUsers(usersString) | ||||
| 		HandleError(parseErr, "Failed to parse users") | ||||
|  | ||||
| 		// Start server | ||||
| 		log.Info().Msg("Starting server") | ||||
| 		api.Run(config, userList) | ||||
| 		// Create oauth whitelist | ||||
| 		oauthWhitelist := utils.ParseCommaString(config.OAuthWhitelist) | ||||
|  | ||||
| 		// Create OAuth config | ||||
| 		oauthConfig := types.OAuthConfig{ | ||||
| 			GithubClientId:      config.GithubClientId, | ||||
| 			GithubClientSecret:  config.GithubClientSecret, | ||||
| 			GoogleClientId:      config.GoogleClientId, | ||||
| 			GoogleClientSecret:  config.GoogleClientSecret, | ||||
| 			GenericClientId:     config.GenericClientId, | ||||
| 			GenericClientSecret: config.GenericClientSecret, | ||||
| 			GenericScopes:       utils.ParseCommaString(config.GenericScopes), | ||||
| 			GenericAuthURL:      config.GenericAuthURL, | ||||
| 			GenericTokenURL:     config.GenericTokenURL, | ||||
| 			GenericUserURL:      config.GenericUserURL, | ||||
| 			AppURL:              config.AppURL, | ||||
| 		} | ||||
|  | ||||
| 		// Create auth service | ||||
| 		auth := auth.NewAuth(users, oauthWhitelist) | ||||
|  | ||||
| 		// Create OAuth providers service | ||||
| 		providers := providers.NewProviders(oauthConfig) | ||||
|  | ||||
| 		// Initialize providers | ||||
| 		providers.Init() | ||||
|  | ||||
| 		// Create hooks service | ||||
| 		hooks := hooks.NewHooks(auth, providers) | ||||
|  | ||||
| 		// Create API | ||||
| 		api := api.NewAPI(types.APIConfig{ | ||||
| 			Port:            config.Port, | ||||
| 			Address:         config.Address, | ||||
| 			Secret:          config.Secret, | ||||
| 			AppURL:          config.AppURL, | ||||
| 			CookieSecure:    config.CookieSecure, | ||||
| 			DisableContinue: config.DisableContinue, | ||||
| 		}, hooks, auth, providers) | ||||
|  | ||||
| 		// Setup routes | ||||
| 		api.Init() | ||||
| 		api.SetupRoutes() | ||||
|  | ||||
| 		// Start | ||||
| 		api.Run() | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| func Execute() { | ||||
| 	err := rootCmd.Execute() | ||||
| 	if err != nil { | ||||
| 		os.Exit(1) | ||||
| 		log.Fatal().Err(err).Msg("Failed to execute command") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func HandleError(err error, msg string) { | ||||
| 	if err != nil { | ||||
| 		log.Fatal().Err(err).Msg(msg) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	rootCmd.AddCommand(cmd.UserCmd()) | ||||
| 	viper.AutomaticEnv() | ||||
| 	rootCmd.Flags().IntP("port", "p", 3000, "Port to run the server on.") | ||||
| 	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("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().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("google-client-id", "", "Google OAuth client ID.") | ||||
| 	rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.") | ||||
| 	rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.") | ||||
| 	rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.") | ||||
| 	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-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") | ||||
| 	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("google-client-id", "GOOGLE_CLIENT_ID") | ||||
| 	viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET") | ||||
| 	viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID") | ||||
| 	viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET") | ||||
| 	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("cookie-expiry", "COOKIE_EXPIRY") | ||||
| 	viper.BindPFlags(rootCmd.Flags()) | ||||
| } | ||||
|   | ||||
							
								
								
									
										79
									
								
								cmd/user/create/create.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								cmd/user/create/create.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| package create | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/charmbracelet/huh" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	"github.com/spf13/cobra" | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
| ) | ||||
|  | ||||
| var interactive bool | ||||
| var username string | ||||
| var password string | ||||
| var docker bool | ||||
|  | ||||
| var CreateCmd = &cobra.Command{ | ||||
| 	Use:   "create", | ||||
| 	Short: "Create a user", | ||||
| 	Long:  `Create a user either interactively or by passing flags.`, | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 		if interactive { | ||||
| 			form := huh.NewForm( | ||||
| 				huh.NewGroup( | ||||
| 					huh.NewInput().Title("Username").Value(&username).Validate((func(s string) error { | ||||
| 						if s == "" { | ||||
| 							return errors.New("username cannot be empty") | ||||
| 						} | ||||
| 						return nil | ||||
| 					})), | ||||
| 					huh.NewInput().Title("Password").Value(&password).Validate((func(s string) error { | ||||
| 						if s == "" { | ||||
| 							return errors.New("password cannot be empty") | ||||
| 						} | ||||
| 						return nil | ||||
| 					})), | ||||
| 					huh.NewSelect[bool]().Title("Format the output for docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&docker), | ||||
| 				), | ||||
| 			) | ||||
|  | ||||
| 			var baseTheme *huh.Theme = huh.ThemeBase() | ||||
|  | ||||
| 			formErr := form.WithTheme(baseTheme).Run() | ||||
|  | ||||
| 			if formErr != nil { | ||||
| 				log.Fatal().Err(formErr).Msg("Form failed") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if username == "" || password == "" { | ||||
| 			log.Error().Msg("Username and password cannot be empty") | ||||
| 		} | ||||
|  | ||||
| 		log.Info().Str("username", username).Str("password", password).Bool("docker", docker).Msg("Creating user") | ||||
|  | ||||
| 		passwordByte, passwordErr := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) | ||||
|  | ||||
| 		if passwordErr != nil { | ||||
| 			log.Fatal().Err(passwordErr).Msg("Failed to hash password") | ||||
| 		} | ||||
|  | ||||
| 		passwordString := string(passwordByte) | ||||
|  | ||||
| 		if docker { | ||||
| 			passwordString = strings.ReplaceAll(passwordString, "$", "$$") | ||||
| 		} | ||||
|  | ||||
| 		log.Info().Str("user", fmt.Sprintf("%s:%s", username, passwordString)).Msg("User created") | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	CreateCmd.Flags().BoolVar(&interactive, "interactive", false, "Create a user interactively") | ||||
| 	CreateCmd.Flags().BoolVar(&docker, "docker", false, "Format output for docker") | ||||
| 	CreateCmd.Flags().StringVar(&username, "username", "", "Username") | ||||
| 	CreateCmd.Flags().StringVar(&password, "password", "", "Password") | ||||
| } | ||||
							
								
								
									
										19
									
								
								cmd/user/user.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								cmd/user/user.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"tinyauth/cmd/user/create" | ||||
| 	"tinyauth/cmd/user/verify" | ||||
|  | ||||
| 	"github.com/spf13/cobra" | ||||
| ) | ||||
|  | ||||
| func UserCmd() *cobra.Command { | ||||
| 	userCmd := &cobra.Command{ | ||||
| 		Use:  "user", | ||||
| 		Short: "User utilities", | ||||
| 		Long: `Utilities for creating and verifying tinyauth compatible users.`, | ||||
| 	} | ||||
| 	userCmd.AddCommand(create.CreateCmd) | ||||
| 	userCmd.AddCommand(verify.VerifyCmd) | ||||
| 	return userCmd | ||||
| }	 | ||||
							
								
								
									
										90
									
								
								cmd/user/verify/verify.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								cmd/user/verify/verify.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| package verify | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/charmbracelet/huh" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	"github.com/spf13/cobra" | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
| ) | ||||
|  | ||||
| var interactive bool | ||||
| var username string | ||||
| var password string | ||||
| var docker bool | ||||
| 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.`, | ||||
| 	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 { | ||||
| 						if s == "" { | ||||
| 							return errors.New("user cannot be empty") | ||||
| 						} | ||||
| 						return nil | ||||
| 					})), | ||||
| 					huh.NewInput().Title("Username").Value(&username).Validate((func(s string) error { | ||||
| 						if s == "" { | ||||
| 							return errors.New("username cannot be empty") | ||||
| 						} | ||||
| 						return nil | ||||
| 					})), | ||||
| 					huh.NewInput().Title("Password").Value(&password).Validate((func(s string) error { | ||||
| 						if s == "" { | ||||
| 							return errors.New("password cannot be empty") | ||||
| 						} | ||||
| 						return nil | ||||
| 					})), | ||||
| 					huh.NewSelect[bool]().Title("Is the user formatted for docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&docker), | ||||
| 				), | ||||
| 			) | ||||
|  | ||||
| 			var baseTheme *huh.Theme = huh.ThemeBase() | ||||
|  | ||||
| 			formErr := form.WithTheme(baseTheme).Run() | ||||
|  | ||||
| 			if formErr != nil { | ||||
| 				log.Fatal().Err(formErr).Msg("Form failed") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if username == "" || password == "" || user == "" { | ||||
| 			log.Fatal().Msg("Username, password and user cannot be empty") | ||||
| 		} | ||||
|  | ||||
| 		log.Info().Str("user", user).Str("username", username).Str("password", password).Bool("docker", docker).Msg("Verifying user") | ||||
|  | ||||
| 		userSplit := strings.Split(user, ":") | ||||
|  | ||||
| 		if userSplit[1] == "" { | ||||
| 			log.Fatal().Msg("User is not formatted correctly") | ||||
| 		} | ||||
|  | ||||
| 		if docker { | ||||
| 			userSplit[1] = strings.ReplaceAll(userSplit[1], "$$", "$") | ||||
| 		} | ||||
|  | ||||
| 		verifyErr := bcrypt.CompareHashAndPassword([]byte(userSplit[1]), []byte(password)) | ||||
|  | ||||
| 		if verifyErr != nil || username != userSplit[0] { | ||||
| 			log.Fatal().Msg("Username or password incorrect") | ||||
| 		} else { | ||||
| 			log.Info().Msg("Verification successful") | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	VerifyCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively") | ||||
| 	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)") | ||||
| } | ||||
							
								
								
									
										34
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								go.mod
									
									
									
									
									
								
							| @@ -3,23 +3,38 @@ module tinyauth | ||||
| go 1.23.2 | ||||
|  | ||||
| require ( | ||||
| 	github.com/gin-contrib/sessions v1.0.2 | ||||
| 	github.com/gin-gonic/gin v1.10.0 | ||||
| 	github.com/go-playground/validator/v10 v10.24.0 | ||||
| 	github.com/google/go-querystring v1.1.0 | ||||
| 	github.com/rs/zerolog v1.33.0 | ||||
| 	github.com/spf13/cobra v1.8.1 | ||||
| 	github.com/spf13/viper v1.19.0 | ||||
| 	golang.org/x/crypto v0.32.0 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/atotto/clipboard v0.1.4 // indirect | ||||
| 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect | ||||
| 	github.com/bytedance/sonic v1.12.7 // indirect | ||||
| 	github.com/bytedance/sonic/loader v0.2.3 // indirect | ||||
| 	github.com/catppuccin/go v0.2.0 // indirect | ||||
| 	github.com/charmbracelet/bubbles v0.20.0 // indirect | ||||
| 	github.com/charmbracelet/bubbletea v1.1.0 // indirect | ||||
| 	github.com/charmbracelet/huh v0.6.0 // indirect | ||||
| 	github.com/charmbracelet/lipgloss v0.13.0 // indirect | ||||
| 	github.com/charmbracelet/x/ansi v0.2.3 // indirect | ||||
| 	github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect | ||||
| 	github.com/charmbracelet/x/term v0.2.0 // indirect | ||||
| 	github.com/cloudwego/base64x v0.1.4 // indirect | ||||
| 	github.com/dustin/go-humanize v1.0.1 // indirect | ||||
| 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | ||||
| 	github.com/fsnotify/fsnotify v1.7.0 // indirect | ||||
| 	github.com/gabriel-vasile/mimetype v1.4.8 // indirect | ||||
| 	github.com/gin-contrib/sessions v1.0.2 // indirect | ||||
| 	github.com/gin-contrib/sse v1.0.0 // indirect | ||||
| 	github.com/go-playground/locales v0.14.1 // indirect | ||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||
| 	github.com/go-playground/validator/v10 v10.24.0 // indirect | ||||
| 	github.com/goccy/go-json v0.10.4 // indirect | ||||
| 	github.com/google/go-querystring v1.1.0 // indirect | ||||
| 	github.com/gorilla/context v1.1.2 // indirect | ||||
| 	github.com/gorilla/securecookie v1.1.2 // indirect | ||||
| 	github.com/gorilla/sessions v1.2.2 // indirect | ||||
| @@ -28,30 +43,37 @@ require ( | ||||
| 	github.com/json-iterator/go v1.1.12 // indirect | ||||
| 	github.com/klauspost/cpuid/v2 v2.2.9 // indirect | ||||
| 	github.com/leodido/go-urn v1.4.0 // indirect | ||||
| 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect | ||||
| 	github.com/magiconair/properties v1.8.7 // indirect | ||||
| 	github.com/mattn/go-colorable v0.1.14 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.20 // indirect | ||||
| 	github.com/mattn/go-localereader v0.0.1 // indirect | ||||
| 	github.com/mattn/go-runewidth v0.0.16 // indirect | ||||
| 	github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect | ||||
| 	github.com/mitchellh/mapstructure v1.5.0 // indirect | ||||
| 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||||
| 	github.com/modern-go/reflect2 v1.0.2 // indirect | ||||
| 	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect | ||||
| 	github.com/muesli/cancelreader v0.2.2 // indirect | ||||
| 	github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect | ||||
| 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect | ||||
| 	github.com/rs/zerolog v1.33.0 // indirect | ||||
| 	github.com/rivo/uniseg v0.4.7 // indirect | ||||
| 	github.com/sagikazarmark/locafero v0.4.0 // indirect | ||||
| 	github.com/sagikazarmark/slog-shim v0.1.0 // indirect | ||||
| 	github.com/sourcegraph/conc v0.3.0 // indirect | ||||
| 	github.com/spf13/afero v1.11.0 // indirect | ||||
| 	github.com/spf13/cast v1.6.0 // indirect | ||||
| 	github.com/spf13/pflag v1.0.5 // indirect | ||||
| 	github.com/spf13/viper v1.19.0 // indirect | ||||
| 	github.com/subosito/gotenv v1.6.0 // indirect | ||||
| 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | ||||
| 	github.com/ugorji/go/codec v1.2.12 // indirect | ||||
| 	go.uber.org/atomic v1.9.0 // indirect | ||||
| 	go.uber.org/multierr v1.9.0 // indirect | ||||
| 	golang.org/x/arch v0.13.0 // indirect | ||||
| 	golang.org/x/crypto v0.32.0 // indirect | ||||
| 	golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect | ||||
| 	golang.org/x/net v0.34.0 // indirect | ||||
| 	golang.org/x/oauth2 v0.25.0 // indirect | ||||
| 	golang.org/x/sync v0.10.0 // indirect | ||||
| 	golang.org/x/sys v0.29.0 // indirect | ||||
| 	golang.org/x/text v0.21.0 // indirect | ||||
| 	google.golang.org/protobuf v1.36.3 // indirect | ||||
|   | ||||
							
								
								
									
										70
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										70
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,16 +1,43 @@ | ||||
| github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= | ||||
| github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= | ||||
| github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= | ||||
| github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= | ||||
| github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q= | ||||
| github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I= | ||||
| github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= | ||||
| github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= | ||||
| github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= | ||||
| github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= | ||||
| github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= | ||||
| github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= | ||||
| github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= | ||||
| github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= | ||||
| github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= | ||||
| github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= | ||||
| github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= | ||||
| github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= | ||||
| github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= | ||||
| github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= | ||||
| github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= | ||||
| github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= | ||||
| github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= | ||||
| github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= | ||||
| github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= | ||||
| github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= | ||||
| github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= | ||||
| github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= | ||||
| github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= | ||||
| github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= | ||||
| github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= | ||||
| github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= | ||||
| github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= | ||||
| github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= | ||||
| github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= | ||||
| github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= | ||||
| github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= | ||||
| github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= | ||||
| github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= | ||||
| @@ -33,11 +60,13 @@ github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= | ||||
| github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= | ||||
| github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | ||||
| github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= | ||||
| github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= | ||||
| github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||||
| github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= | ||||
| github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= | ||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||
| github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= | ||||
| github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||
| github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= | ||||
| github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= | ||||
| github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= | ||||
| @@ -54,8 +83,14 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 | ||||
| github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= | ||||
| github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= | ||||
| github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= | ||||
| github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= | ||||
| github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= | ||||
| github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||||
| github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||||
| github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= | ||||
| github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= | ||||
| github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= | ||||
| github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= | ||||
| github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= | ||||
| github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= | ||||
| github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= | ||||
| @@ -65,6 +100,12 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ | ||||
| github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||||
| github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= | ||||
| github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||||
| github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= | ||||
| github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= | ||||
| github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= | ||||
| github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | ||||
| github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= | ||||
| github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= | ||||
| github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= | ||||
| github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= | ||||
| github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||
| @@ -72,11 +113,23 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w | ||||
| github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||
| github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= | ||||
| github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | ||||
| github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= | ||||
| github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= | ||||
| github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= | ||||
| github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= | ||||
| github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= | ||||
| github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= | ||||
| github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= | ||||
| github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= | ||||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= | ||||
| github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||||
| github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= | ||||
| github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | ||||
| github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= | ||||
| github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= | ||||
| github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= | ||||
| github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= | ||||
| github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= | ||||
| @@ -127,6 +180,11 @@ golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjs | ||||
| golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= | ||||
| golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= | ||||
| golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= | ||||
| golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= | ||||
| golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= | ||||
| golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= | ||||
| golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||
| golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| @@ -134,12 +192,12 @@ golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= | ||||
| golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= | ||||
| golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= | ||||
| google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= | ||||
| gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= | ||||
| gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import ( | ||||
| 	"tinyauth/internal/assets" | ||||
| 	"tinyauth/internal/auth" | ||||
| 	"tinyauth/internal/hooks" | ||||
| 	"tinyauth/internal/providers" | ||||
| 	"tinyauth/internal/types" | ||||
| 	"tinyauth/internal/utils" | ||||
|  | ||||
| @@ -20,32 +21,58 @@ import ( | ||||
| 	"github.com/rs/zerolog/log" | ||||
| ) | ||||
|  | ||||
| func Run(config types.Config, users types.UserList) { | ||||
| func NewAPI(config types.APIConfig, hooks *hooks.Hooks, auth *auth.Auth, providers *providers.Providers) *API { | ||||
| 	return &API{ | ||||
| 		Config:    config, | ||||
| 		Hooks:     hooks, | ||||
| 		Auth:      auth, | ||||
| 		Providers: providers, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type API struct { | ||||
| 	Config    types.APIConfig | ||||
| 	Router    *gin.Engine | ||||
| 	Hooks     *hooks.Hooks | ||||
| 	Auth      *auth.Auth | ||||
| 	Providers *providers.Providers | ||||
| 	Domain    string | ||||
| } | ||||
|  | ||||
| func (api *API) Init() { | ||||
| 	gin.SetMode(gin.ReleaseMode) | ||||
|  | ||||
| 	router := gin.New() | ||||
| 	router.Use(zerolog()) | ||||
| 	dist, distErr := fs.Sub(assets.Assets, "dist") | ||||
|  | ||||
| 	if distErr != nil { | ||||
| 		log.Fatal().Err(distErr).Msg("Failed to get UI assets") | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
|  | ||||
| 	fileServer := http.FileServer(http.FS(dist)) | ||||
| 	store := cookie.NewStore([]byte(config.Secret)) | ||||
| 	store := cookie.NewStore([]byte(api.Config.Secret)) | ||||
|  | ||||
| 	domain, domainErr := utils.GetRootURL(config.AppURL) | ||||
| 	domain, domainErr := utils.GetRootURL(api.Config.AppURL) | ||||
|  | ||||
| 	if domainErr != nil { | ||||
| 		log.Fatal().Err(domainErr).Msg("Failed to get domain") | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	log.Info().Str("domain", domain).Msg("Using domain for cookies") | ||||
|  | ||||
| 	api.Domain = fmt.Sprintf(".%s", domain) | ||||
|  | ||||
| 	store.Options(sessions.Options{ | ||||
| 		Domain: fmt.Sprintf(".%s", domain), | ||||
| 		Path: "/", | ||||
| 		Domain:   api.Domain, | ||||
| 		Path:     "/", | ||||
| 		HttpOnly: true, | ||||
| 		Secure:   api.Config.CookieSecure, | ||||
| 		MaxAge:   api.Config.CookieExpiry, | ||||
| 	}) | ||||
|   	router.Use(sessions.Sessions("tinyauth", store)) | ||||
|  | ||||
| 	router.Use(sessions.Sessions("tinyauth", store)) | ||||
|  | ||||
| 	router.Use(func(c *gin.Context) { | ||||
| 		if !strings.HasPrefix(c.Request.URL.Path, "/api") { | ||||
| @@ -58,12 +85,25 @@ func Run(config types.Config, users types.UserList) { | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	router.GET("/api/auth", func (c *gin.Context) { | ||||
| 		userContext := hooks.UseUserContext(c, users) | ||||
| 	api.Router = router | ||||
| } | ||||
|  | ||||
| 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 | ||||
| 		} | ||||
|  | ||||
| 		if userContext.IsLoggedIn { | ||||
| 			c.JSON(200, gin.H{ | ||||
| 				"status": 200, | ||||
| 				"status":  200, | ||||
| 				"message": "Authenticated", | ||||
| 			}) | ||||
| 			return | ||||
| @@ -77,97 +117,233 @@ func Run(config types.Config, users types.UserList) { | ||||
| 		}) | ||||
|  | ||||
| 		if queryErr != nil { | ||||
| 			log.Error().Err(queryErr).Msg("Failed to build query") | ||||
| 			c.JSON(501, gin.H{ | ||||
| 				"status": 501, | ||||
| 				"status":  501, | ||||
| 				"message": "Internal Server Error", | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/?%s", config.AppURL, queries.Encode())) | ||||
| 		c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/?%s", api.Config.AppURL, queries.Encode())) | ||||
| 	}) | ||||
|  | ||||
| 	router.POST("/api/login", func (c *gin.Context) { | ||||
| 	api.Router.POST("/api/login", func(c *gin.Context) { | ||||
| 		var login types.LoginRequest | ||||
|  | ||||
| 		err := c.BindJSON(&login) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			log.Error().Err(err).Msg("Failed to bind JSON") | ||||
| 			c.JSON(400, gin.H{ | ||||
| 				"status": 400, | ||||
| 				"status":  400, | ||||
| 				"message": "Bad Request", | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		user := auth.FindUser(users, login.Username) | ||||
| 		user := api.Auth.GetUser(login.Email) | ||||
|  | ||||
| 		if user == nil { | ||||
| 			c.JSON(401, gin.H{ | ||||
| 				"status": 401, | ||||
| 				"status":  401, | ||||
| 				"message": "Unauthorized", | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if !auth.CheckPassword(*user, login.Password) { | ||||
| 		if !api.Auth.CheckPassword(*user, login.Password) { | ||||
| 			c.JSON(401, gin.H{ | ||||
| 				"status": 401, | ||||
| 				"status":  401, | ||||
| 				"message": "Unauthorized", | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		session := sessions.Default(c) | ||||
| 		session.Set("tinyauth", user.Username) | ||||
| 		session.Set("tinyauth_sid", fmt.Sprintf("email:%s", login.Email)) | ||||
| 		session.Save() | ||||
|  | ||||
| 		c.JSON(200, gin.H{ | ||||
| 			"status": 200, | ||||
| 			"status":  200, | ||||
| 			"message": "Logged in", | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	router.POST("/api/logout", func (c *gin.Context) { | ||||
| 	api.Router.POST("/api/logout", func(c *gin.Context) { | ||||
| 		session := sessions.Default(c) | ||||
| 		session.Delete("tinyauth") | ||||
| 		session.Delete("tinyauth_sid") | ||||
| 		session.Save() | ||||
|  | ||||
| 		c.SetCookie("tinyauth_redirect_uri", "", -1, "/", api.Domain, api.Config.CookieSecure, true) | ||||
|  | ||||
| 		c.JSON(200, gin.H{ | ||||
| 			"status": 200, | ||||
| 			"status":  200, | ||||
| 			"message": "Logged out", | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	router.GET("/api/status", func (c *gin.Context) { | ||||
| 		userContext := hooks.UseUserContext(c, users) | ||||
| 	api.Router.GET("/api/status", 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 | ||||
| 		} | ||||
|  | ||||
| 		if !userContext.IsLoggedIn { | ||||
| 			c.JSON(200, gin.H{ | ||||
| 				"status": 200, | ||||
| 				"message": "Unauthenticated", | ||||
| 				"username": "", | ||||
| 				"isLoggedIn": false, | ||||
| 				"status":              200, | ||||
| 				"message":             "Unauthenticated", | ||||
| 				"email":               "", | ||||
| 				"isLoggedIn":          false, | ||||
| 				"oauth":               false, | ||||
| 				"provider":            "", | ||||
| 				"configuredProviders": api.Providers.GetConfiguredProviders(), | ||||
| 				"disableContinue":     api.Config.DisableContinue, | ||||
| 			}) | ||||
| 			return | ||||
| 		}  | ||||
| 		} | ||||
|  | ||||
| 		c.JSON(200, gin.H{ | ||||
| 			"status": 200, | ||||
| 			"message": "Authenticated", | ||||
| 			"username": userContext.Username, | ||||
| 			"isLoggedIn": true, | ||||
| 			"status":              200, | ||||
| 			"message":             "Authenticated", | ||||
| 			"email":               userContext.Email, | ||||
| 			"isLoggedIn":          userContext.IsLoggedIn, | ||||
| 			"oauth":               userContext.OAuth, | ||||
| 			"provider":            userContext.Provider, | ||||
| 			"configuredProviders": api.Providers.GetConfiguredProviders(), | ||||
| 			"disableContinue":     api.Config.DisableContinue, | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	router.GET("/api/healthcheck", func (c *gin.Context) { | ||||
| 	api.Router.GET("/api/healthcheck", func(c *gin.Context) { | ||||
| 		c.JSON(200, gin.H{ | ||||
| 			"status": 200, | ||||
| 			"status":  200, | ||||
| 			"message": "OK", | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	router.Run(fmt.Sprintf("%s:%d", config.Address, config.Port)) | ||||
| 	api.Router.GET("/api/oauth/url/:provider", func(c *gin.Context) { | ||||
| 		var request types.OAuthRequest | ||||
|  | ||||
| 		bindErr := c.BindUri(&request) | ||||
|  | ||||
| 		if bindErr != nil { | ||||
| 			log.Error().Err(bindErr).Msg("Failed to bind URI") | ||||
| 			c.JSON(400, gin.H{ | ||||
| 				"status":  400, | ||||
| 				"message": "Bad Request", | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		provider := api.Providers.GetProvider(request.Provider) | ||||
|  | ||||
| 		if provider == nil { | ||||
| 			c.JSON(404, gin.H{ | ||||
| 				"status":  404, | ||||
| 				"message": "Not Found", | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		authURL := provider.GetAuthURL() | ||||
|  | ||||
| 		redirectURI := c.Query("redirect_uri") | ||||
|  | ||||
| 		if redirectURI != "" { | ||||
| 			c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", api.Domain, api.Config.CookieSecure, true) | ||||
| 		} | ||||
|  | ||||
| 		c.JSON(200, gin.H{ | ||||
| 			"status":  200, | ||||
| 			"message": "Ok", | ||||
| 			"url":     authURL, | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	api.Router.GET("/api/oauth/callback/:provider", func(c *gin.Context) { | ||||
| 		var providerName types.OAuthRequest | ||||
|  | ||||
| 		bindErr := c.BindUri(&providerName) | ||||
|  | ||||
| 		if handleApiError(c, "Failed to bind URI", bindErr) { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		code := c.Query("code") | ||||
|  | ||||
| 		if code == "" { | ||||
| 			log.Error().Msg("No code provided") | ||||
| 			c.Redirect(http.StatusPermanentRedirect, "/error") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		provider := api.Providers.GetProvider(providerName.Provider) | ||||
|  | ||||
| 		if provider == nil { | ||||
| 			c.Redirect(http.StatusPermanentRedirect, "/not-found") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		token, tokenErr := provider.ExchangeToken(code) | ||||
|  | ||||
| 		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() | ||||
|  | ||||
| 		redirectURI, redirectURIErr := c.Cookie("tinyauth_redirect_uri") | ||||
|  | ||||
| 		if redirectURIErr != nil { | ||||
| 			c.JSON(200, gin.H{ | ||||
| 				"status":  200, | ||||
| 				"message": "Logged in", | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		c.SetCookie("tinyauth_redirect_uri", "", -1, "/", api.Domain, api.Config.CookieSecure, true) | ||||
|  | ||||
| 		redirectQuery, redirectQueryErr := query.Values(types.LoginQuery{ | ||||
| 			RedirectURI: redirectURI, | ||||
| 		}) | ||||
|  | ||||
| 		if handleApiError(c, "Failed to build query", redirectQueryErr) { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/continue?%s", api.Config.AppURL, redirectQuery.Encode())) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (api *API) Run() { | ||||
| 	log.Info().Str("address", api.Config.Address).Int("port", api.Config.Port).Msg("Starting server") | ||||
| 	api.Router.Run(fmt.Sprintf("%s:%d", api.Config.Address, api.Config.Port)) | ||||
| } | ||||
|  | ||||
| func zerolog() gin.HandlerFunc { | ||||
| @@ -180,16 +356,25 @@ func zerolog() gin.HandlerFunc { | ||||
| 		address := c.Request.RemoteAddr | ||||
| 		method := c.Request.Method | ||||
| 		path := c.Request.URL.Path | ||||
| 		 | ||||
|  | ||||
| 		latency := time.Since(tStart).String() | ||||
|  | ||||
| 		switch { | ||||
| 			case code >= 200 && code < 300: | ||||
| 				log.Info().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request") | ||||
| 			case code >= 300 && code < 400: | ||||
| 				log.Warn().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request") | ||||
| 			case code >= 400: | ||||
| 				log.Error().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request") | ||||
| 		case code >= 200 && code < 300: | ||||
| 			log.Info().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request") | ||||
| 		case code >= 300 && code < 400: | ||||
| 			log.Warn().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request") | ||||
| 		case code >= 400: | ||||
| 			log.Error().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request") | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| } | ||||
|  | ||||
| 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 | ||||
| } | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| v0.2.0 | ||||
| v1.0.0 | ||||
| @@ -6,16 +6,40 @@ import ( | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
| ) | ||||
|  | ||||
| func FindUser(userList types.UserList, username string) (*types.User) { | ||||
| 	for _, user := range userList.Users { | ||||
| 		if user.Username == username { | ||||
| func NewAuth(userList types.Users, oauthWhitelist []string) *Auth { | ||||
| 	return &Auth{ | ||||
| 		Users:          userList, | ||||
| 		OAuthWhitelist: oauthWhitelist, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type Auth struct { | ||||
| 	Users          types.Users | ||||
| 	OAuthWhitelist []string | ||||
| } | ||||
|  | ||||
| func (auth *Auth) GetUser(email string) *types.User { | ||||
| 	for _, user := range auth.Users { | ||||
| 		if user.Email == email { | ||||
| 			return &user | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func CheckPassword(user types.User, password string) bool { | ||||
| 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 | ||||
| } | ||||
|   | ||||
| @@ -1,44 +1,125 @@ | ||||
| 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" | ||||
| ) | ||||
|  | ||||
| func UseUserContext(c *gin.Context, userList types.UserList) (types.UserContext) { | ||||
| func NewHooks(auth *auth.Auth, providers *providers.Providers) *Hooks { | ||||
| 	return &Hooks{ | ||||
| 		Auth:      auth, | ||||
| 		Providers: providers, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type Hooks struct { | ||||
| 	Auth      *auth.Auth | ||||
| 	Providers *providers.Providers | ||||
| } | ||||
|  | ||||
| func (hooks *Hooks) UseUserContext(c *gin.Context) (types.UserContext, error) { | ||||
| 	session := sessions.Default(c) | ||||
| 	cookie := session.Get("tinyauth") | ||||
| 	sessionCookie := session.Get("tinyauth_sid") | ||||
|  | ||||
| 	if cookie == nil { | ||||
| 	if sessionCookie == nil { | ||||
| 		return types.UserContext{ | ||||
| 			Username: "", | ||||
| 			Email:      "", | ||||
| 			IsLoggedIn: false, | ||||
| 		} | ||||
| 			OAuth:      false, | ||||
| 			Provider:   "", | ||||
| 		}, nil | ||||
| 	} | ||||
|  | ||||
| 	username, ok := cookie.(string) | ||||
| 	data, dataOk := sessionCookie.(string) | ||||
|  | ||||
| 	if !ok { | ||||
| 	if !dataOk { | ||||
| 		return types.UserContext{ | ||||
| 			Username: "", | ||||
| 			Email:      "", | ||||
| 			IsLoggedIn: false, | ||||
| 		} | ||||
| 			OAuth:      false, | ||||
| 			Provider:   "", | ||||
| 		}, nil | ||||
| 	} | ||||
|  | ||||
| 	user := auth.FindUser(userList, username) | ||||
| 	split := strings.Split(data, ":") | ||||
|  | ||||
| 	if user == nil { | ||||
| 	if len(split) != 2 { | ||||
| 		return types.UserContext{ | ||||
| 			Username: "", | ||||
| 			Email:      "", | ||||
| 			IsLoggedIn: false, | ||||
| 			OAuth:      false, | ||||
| 			Provider:   "", | ||||
| 		}, nil | ||||
| 	} | ||||
|  | ||||
| 	sessionType := split[0] | ||||
| 	sessionValue := split[1] | ||||
|  | ||||
| 	if sessionType == "email" { | ||||
| 		user := hooks.Auth.GetUser(sessionValue) | ||||
| 		if user == nil { | ||||
| 			return types.UserContext{ | ||||
| 				Email:      "", | ||||
| 				IsLoggedIn: false, | ||||
| 				OAuth:      false, | ||||
| 				Provider:   "", | ||||
| 			}, nil | ||||
| 		} | ||||
| 		return types.UserContext{ | ||||
| 			Email:      sessionValue, | ||||
| 			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 | ||||
| 	} | ||||
|  | ||||
| 	return types.UserContext{ | ||||
| 		Username: username, | ||||
| 		Email:      email, | ||||
| 		IsLoggedIn: true, | ||||
| 	} | ||||
| } | ||||
| 		OAuth:      true, | ||||
| 		Provider:   sessionType, | ||||
| 	}, nil | ||||
| } | ||||
|   | ||||
							
								
								
									
										43
									
								
								internal/oauth/oauth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								internal/oauth/oauth.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| package oauth | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"golang.org/x/oauth2" | ||||
| ) | ||||
|  | ||||
| func NewOAuth(config oauth2.Config) *OAuth { | ||||
| 	return &OAuth{ | ||||
| 		Config: config, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type OAuth struct { | ||||
| 	Config   oauth2.Config | ||||
| 	Context  context.Context | ||||
| 	Token    *oauth2.Token | ||||
| 	Verifier string | ||||
| } | ||||
|  | ||||
| func (oauth *OAuth) Init() { | ||||
| 	oauth.Context = context.Background() | ||||
| 	oauth.Verifier = oauth2.GenerateVerifier() | ||||
| } | ||||
|  | ||||
| func (oauth *OAuth) GetAuthURL() string { | ||||
| 	return oauth.Config.AuthCodeURL("state", oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(oauth.Verifier)) | ||||
| } | ||||
|  | ||||
| func (oauth *OAuth) ExchangeToken(code string) (string, error) { | ||||
| 	token, err := oauth.Config.Exchange(oauth.Context, code, oauth2.VerifierOption(oauth.Verifier)) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	oauth.Token = token | ||||
| 	return oauth.Token.AccessToken, nil | ||||
| } | ||||
|  | ||||
| func (oauth *OAuth) GetClient() *http.Client { | ||||
| 	return oauth.Config.Client(oauth.Context, oauth.Token) | ||||
| } | ||||
							
								
								
									
										35
									
								
								internal/providers/generic.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								internal/providers/generic.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| package providers | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| type GenericUserInfoResponse struct { | ||||
| 	Email string `json:"email"` | ||||
| } | ||||
|  | ||||
| func GetGenericEmail(client *http.Client, url string) (string, error) { | ||||
| 	res, resErr := client.Get(url) | ||||
|  | ||||
| 	if resErr != nil { | ||||
| 		return "", resErr | ||||
| 	} | ||||
|  | ||||
| 	body, bodyErr := io.ReadAll(res.Body) | ||||
|  | ||||
| 	if bodyErr != nil { | ||||
| 		return "", bodyErr | ||||
| 	} | ||||
|  | ||||
| 	var user GenericUserInfoResponse | ||||
|  | ||||
| 	jsonErr := json.Unmarshal(body, &user) | ||||
|  | ||||
| 	if jsonErr != nil { | ||||
| 		return "", jsonErr | ||||
| 	} | ||||
|  | ||||
| 	return user.Email, nil | ||||
| } | ||||
							
								
								
									
										47
									
								
								internal/providers/github.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								internal/providers/github.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| package providers | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| type GithubUserInfoResponse []struct { | ||||
| 	Email   string `json:"email"` | ||||
| 	Primary bool   `json:"primary"` | ||||
| } | ||||
|  | ||||
| func GithubScopes() []string { | ||||
| 	return []string{"user:email"} | ||||
| } | ||||
|  | ||||
| func GetGithubEmail(client *http.Client) (string, error) { | ||||
| 	res, resErr := client.Get("https://api.github.com/user/emails") | ||||
|  | ||||
| 	if resErr != nil { | ||||
| 		return "", resErr | ||||
| 	} | ||||
|  | ||||
| 	body, bodyErr := io.ReadAll(res.Body) | ||||
|  | ||||
| 	if bodyErr != nil { | ||||
| 		return "", bodyErr | ||||
| 	} | ||||
|  | ||||
| 	var emails GithubUserInfoResponse | ||||
|  | ||||
| 	jsonErr := json.Unmarshal(body, &emails) | ||||
|  | ||||
| 	if jsonErr != nil { | ||||
| 		return "", jsonErr | ||||
| 	} | ||||
|  | ||||
| 	for _, email := range emails { | ||||
| 		if email.Primary { | ||||
| 			return email.Email, nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return "", errors.New("no primary email found") | ||||
| } | ||||
							
								
								
									
										39
									
								
								internal/providers/google.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								internal/providers/google.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| package providers | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| type GoogleUserInfoResponse struct { | ||||
| 	Email string `json:"email"` | ||||
| } | ||||
|  | ||||
| func GoogleScopes() []string { | ||||
| 	return []string{"https://www.googleapis.com/auth/userinfo.email"} | ||||
| } | ||||
|  | ||||
| func GetGoogleEmail(client *http.Client) (string, error) { | ||||
| 	res, resErr := client.Get("https://www.googleapis.com/userinfo/v2/me") | ||||
|  | ||||
| 	if resErr != nil { | ||||
| 		return "", resErr | ||||
| 	} | ||||
|  | ||||
| 	body, bodyErr := io.ReadAll(res.Body) | ||||
|  | ||||
| 	if bodyErr != nil { | ||||
| 		return "", bodyErr | ||||
| 	} | ||||
|  | ||||
| 	var user GoogleUserInfoResponse | ||||
|  | ||||
| 	jsonErr := json.Unmarshal(body, &user) | ||||
|  | ||||
| 	if jsonErr != nil { | ||||
| 		return "", jsonErr | ||||
| 	} | ||||
|  | ||||
| 	return user.Email, nil | ||||
| } | ||||
							
								
								
									
										127
									
								
								internal/providers/providers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								internal/providers/providers.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | ||||
| package providers | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"tinyauth/internal/oauth" | ||||
| 	"tinyauth/internal/types" | ||||
|  | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	"golang.org/x/oauth2" | ||||
| 	"golang.org/x/oauth2/endpoints" | ||||
| ) | ||||
|  | ||||
| func NewProviders(config types.OAuthConfig) *Providers { | ||||
| 	return &Providers{ | ||||
| 		Config: config, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type Providers struct { | ||||
| 	Config  types.OAuthConfig | ||||
| 	Github  *oauth.OAuth | ||||
| 	Google  *oauth.OAuth | ||||
| 	Generic *oauth.OAuth | ||||
| } | ||||
|  | ||||
| func (providers *Providers) Init() { | ||||
| 	if providers.Config.GithubClientId != "" && providers.Config.GithubClientSecret != "" { | ||||
| 		log.Info().Msg("Initializing Github OAuth") | ||||
| 		providers.Github = oauth.NewOAuth(oauth2.Config{ | ||||
| 			ClientID:     providers.Config.GithubClientId, | ||||
| 			ClientSecret: providers.Config.GithubClientSecret, | ||||
| 			RedirectURL:  fmt.Sprintf("%s/api/oauth/callback/github", providers.Config.AppURL), | ||||
| 			Scopes:       GithubScopes(), | ||||
| 			Endpoint:     endpoints.GitHub, | ||||
| 		}) | ||||
| 		providers.Github.Init() | ||||
| 	} | ||||
| 	if providers.Config.GoogleClientId != "" && providers.Config.GoogleClientSecret != "" { | ||||
| 		log.Info().Msg("Initializing Google OAuth") | ||||
| 		providers.Google = oauth.NewOAuth(oauth2.Config{ | ||||
| 			ClientID:     providers.Config.GoogleClientId, | ||||
| 			ClientSecret: providers.Config.GoogleClientSecret, | ||||
| 			RedirectURL:  fmt.Sprintf("%s/api/oauth/callback/google", providers.Config.AppURL), | ||||
| 			Scopes:       GoogleScopes(), | ||||
| 			Endpoint:     endpoints.Google, | ||||
| 		}) | ||||
| 		providers.Google.Init() | ||||
| 	} | ||||
| 	if providers.Config.GenericClientId != "" && providers.Config.GenericClientSecret != "" { | ||||
| 		log.Info().Msg("Initializing Generic OAuth") | ||||
| 		providers.Generic = oauth.NewOAuth(oauth2.Config{ | ||||
| 			ClientID:     providers.Config.GenericClientId, | ||||
| 			ClientSecret: providers.Config.GenericClientSecret, | ||||
| 			RedirectURL:  fmt.Sprintf("%s/api/oauth/callback/generic", providers.Config.AppURL), | ||||
| 			Scopes:       providers.Config.GenericScopes, | ||||
| 			Endpoint: oauth2.Endpoint{ | ||||
| 				AuthURL:  providers.Config.GenericAuthURL, | ||||
| 				TokenURL: providers.Config.GenericTokenURL, | ||||
| 			}, | ||||
| 		}) | ||||
| 		providers.Generic.Init() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (providers *Providers) GetProvider(provider string) *oauth.OAuth { | ||||
| 	switch provider { | ||||
| 	case "github": | ||||
| 		return providers.Github | ||||
| 	case "google": | ||||
| 		return providers.Google | ||||
| 	case "generic": | ||||
| 		return providers.Generic | ||||
| 	default: | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (providers *Providers) GetUser(provider string) (string, error) { | ||||
| 	switch provider { | ||||
| 	case "github": | ||||
| 		if providers.Github == nil { | ||||
| 			return "", nil | ||||
| 		} | ||||
| 		client := providers.Github.GetClient() | ||||
| 		email, emailErr := GetGithubEmail(client) | ||||
| 		if emailErr != nil { | ||||
| 			return "", emailErr | ||||
| 		} | ||||
| 		return email, nil | ||||
| 	case "google": | ||||
| 		if providers.Google == nil { | ||||
| 			return "", nil | ||||
| 		} | ||||
| 		client := providers.Google.GetClient() | ||||
| 		email, emailErr := GetGoogleEmail(client) | ||||
| 		if emailErr != nil { | ||||
| 			return "", emailErr | ||||
| 		} | ||||
| 		return email, nil | ||||
| 	case "generic": | ||||
| 		if providers.Generic == nil { | ||||
| 			return "", nil | ||||
| 		} | ||||
| 		client := providers.Generic.GetClient() | ||||
| 		email, emailErr := GetGenericEmail(client, providers.Config.GenericUserURL) | ||||
| 		if emailErr != nil { | ||||
| 			return "", emailErr | ||||
| 		} | ||||
| 		return email, nil | ||||
| 	default: | ||||
| 		return "", nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (provider *Providers) GetConfiguredProviders() []string { | ||||
| 	providers := []string{} | ||||
| 	if provider.Github != nil { | ||||
| 		providers = append(providers, "github") | ||||
| 	} | ||||
| 	if provider.Google != nil { | ||||
| 		providers = append(providers, "google") | ||||
| 	} | ||||
| 	if provider.Generic != nil { | ||||
| 		providers = append(providers, "generic") | ||||
| 	} | ||||
| 	return providers | ||||
| } | ||||
| @@ -1,33 +1,87 @@ | ||||
| package types | ||||
|  | ||||
| import "tinyauth/internal/oauth" | ||||
|  | ||||
| type LoginQuery struct { | ||||
| 	RedirectURI string `url:"redirect_uri"` | ||||
| } | ||||
|  | ||||
| type LoginRequest struct { | ||||
| 	Username string `json:"username"` | ||||
| 	Email    string `json:"email"` | ||||
| 	Password string `json:"password"` | ||||
| } | ||||
|  | ||||
| type User struct { | ||||
| 	Username string | ||||
| 	Email    string | ||||
| 	Password string | ||||
| } | ||||
|  | ||||
| type UserList struct { | ||||
| 	Users []User | ||||
| } | ||||
| 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"` | ||||
| 	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"` | ||||
| } | ||||
|  | ||||
| type UserContext struct { | ||||
| 	Username string | ||||
| 	Email      string | ||||
| 	IsLoggedIn bool | ||||
| } | ||||
| 	OAuth      bool | ||||
| 	Provider   string | ||||
| } | ||||
|  | ||||
| type APIConfig struct { | ||||
| 	Port            int | ||||
| 	Address         string | ||||
| 	Secret          string | ||||
| 	AppURL          string | ||||
| 	CookieSecure    bool | ||||
| 	CookieExpiry    int | ||||
| 	DisableContinue bool | ||||
| } | ||||
|  | ||||
| type OAuthConfig struct { | ||||
| 	GithubClientId      string | ||||
| 	GithubClientSecret  string | ||||
| 	GoogleClientId      string | ||||
| 	GoogleClientSecret  string | ||||
| 	GenericClientId     string | ||||
| 	GenericClientSecret string | ||||
| 	GenericScopes       []string | ||||
| 	GenericAuthURL      string | ||||
| 	GenericTokenURL     string | ||||
| 	GenericUserURL      string | ||||
| 	AppURL              string | ||||
| } | ||||
|  | ||||
| type OAuthRequest struct { | ||||
| 	Provider string `uri:"provider" binding:"required"` | ||||
| } | ||||
|  | ||||
| type OAuthProviders struct { | ||||
| 	Github    *oauth.OAuth | ||||
| 	Google    *oauth.OAuth | ||||
| 	Microsoft *oauth.OAuth | ||||
| } | ||||
|  | ||||
| type UnauthorizedQuery struct { | ||||
| 	Email string `url:"email"` | ||||
| } | ||||
|   | ||||
| @@ -8,26 +8,26 @@ import ( | ||||
| 	"tinyauth/internal/types" | ||||
| ) | ||||
|  | ||||
| func ParseUsers(users string) (types.UserList, error) { | ||||
| 	var userList types.UserList | ||||
| 	userListString := strings.Split(users, ",") | ||||
| func ParseUsers(users string) (types.Users, error) { | ||||
| 	var usersParsed types.Users | ||||
| 	userList := strings.Split(users, ",") | ||||
|  | ||||
| 	if len(userListString) == 0 { | ||||
| 		return types.UserList{}, errors.New("invalid user format") | ||||
| 	if len(userList) == 0 { | ||||
| 		return types.Users{}, errors.New("invalid user format") | ||||
| 	} | ||||
|  | ||||
| 	for _, user := range userListString { | ||||
| 	for _, user := range userList { | ||||
| 		userSplit := strings.Split(user, ":") | ||||
| 		if len(userSplit) != 2 { | ||||
| 			return types.UserList{}, errors.New("invalid user format") | ||||
| 			return types.Users{}, errors.New("invalid user format") | ||||
| 		} | ||||
| 		userList.Users = append(userList.Users, types.User{ | ||||
| 			Username: userSplit[0], | ||||
| 		usersParsed = append(usersParsed, types.User{ | ||||
| 			Email:    userSplit[0], | ||||
| 			Password: userSplit[1], | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return userList, nil | ||||
| 	return usersParsed, nil | ||||
| } | ||||
|  | ||||
| func GetRootURL(urlSrc string) (string, error) { | ||||
| @@ -39,7 +39,7 @@ func GetRootURL(urlSrc string) (string, error) { | ||||
|  | ||||
| 	urlSplitted := strings.Split(urlParsed.Host, ".") | ||||
|  | ||||
| 	urlFinal := urlSplitted[len(urlSplitted)-2] + "." + urlSplitted[len(urlSplitted)-1] | ||||
| 	urlFinal := strings.Join(urlSplitted[1:], ".") | ||||
|  | ||||
| 	return urlFinal, nil | ||||
| } | ||||
| @@ -59,3 +59,25 @@ func GetUsersFromFile(usersFile string) (string, error) { | ||||
|  | ||||
| 	return string(data), nil | ||||
| } | ||||
|  | ||||
| func ParseFileToLine(content string) string { | ||||
| 	lines := strings.Split(content, "\n") | ||||
| 	users := make([]string, 0) | ||||
|  | ||||
| 	for _, line := range lines { | ||||
| 		if strings.TrimSpace(line) == "" { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		users = append(users, line) | ||||
| 	} | ||||
|  | ||||
| 	return strings.Join(users, ",") | ||||
| } | ||||
|  | ||||
| func ParseCommaString(str string) []string { | ||||
| 	if str == "" { | ||||
| 		return []string{} | ||||
| 	} | ||||
| 	return strings.Split(str, ",") | ||||
| } | ||||
|   | ||||
							
								
								
									
										15
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								main.go
									
									
									
									
									
								
							| @@ -1,7 +1,20 @@ | ||||
| package main | ||||
|  | ||||
| import "tinyauth/cmd" | ||||
| import ( | ||||
| 	"os" | ||||
| 	"time" | ||||
| 	"tinyauth/cmd" | ||||
| 	"tinyauth/internal/assets" | ||||
|  | ||||
| 	"github.com/rs/zerolog" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	// Logger | ||||
| 	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger() | ||||
| 	log.Info().Str("version", assets.Version).Msg("Starting tinyauth") | ||||
|  | ||||
| 	// Run cmd | ||||
| 	cmd.Execute() | ||||
| } | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 61 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 72 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 66 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 64 KiB | 
							
								
								
									
										18
									
								
								site/src/icons/github.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								site/src/icons/github.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import type { SVGProps } from "react"; | ||||
|  | ||||
| export function GithubIcon(props: SVGProps<SVGSVGElement>) { | ||||
|   return ( | ||||
|     <svg | ||||
|       xmlns="http://www.w3.org/2000/svg" | ||||
|       width={24} | ||||
|       height={24} | ||||
|       viewBox="0 0 24 24" | ||||
|       {...props} | ||||
|     > | ||||
|       <path | ||||
|         fill="currentColor" | ||||
|         d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2" | ||||
|       ></path> | ||||
|     </svg> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										30
									
								
								site/src/icons/google.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								site/src/icons/google.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import type { SVGProps } from "react"; | ||||
|  | ||||
| export function GoogleIcon(props: SVGProps<SVGSVGElement>) { | ||||
|   return ( | ||||
|     <svg | ||||
|       xmlns="http://www.w3.org/2000/svg" | ||||
|       width={48} | ||||
|       height={48} | ||||
|       viewBox="0 0 48 48" | ||||
|       {...props} | ||||
|     > | ||||
|       <path | ||||
|         fill="#ffc107" | ||||
|         d="M43.611 20.083H42V20H24v8h11.303c-1.649 4.657-6.08 8-11.303 8c-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4C12.955 4 4 12.955 4 24s8.955 20 20 20s20-8.955 20-20c0-1.341-.138-2.65-.389-3.917" | ||||
|       ></path> | ||||
|       <path | ||||
|         fill="#ff3d00" | ||||
|         d="m6.306 14.691l6.571 4.819C14.655 15.108 18.961 12 24 12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4C16.318 4 9.656 8.337 6.306 14.691" | ||||
|       ></path> | ||||
|       <path | ||||
|         fill="#4caf50" | ||||
|         d="M24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238A11.9 11.9 0 0 1 24 36c-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025C9.505 39.556 16.227 44 24 44" | ||||
|       ></path> | ||||
|       <path | ||||
|         fill="#1976d2" | ||||
|         d="M43.611 20.083H42V20H24v8h11.303a12.04 12.04 0 0 1-4.087 5.571l.003-.002l6.19 5.238C36.971 39.205 44 34 44 24c0-1.341-.138-2.65-.389-3.917" | ||||
|       ></path> | ||||
|     </svg> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										24
									
								
								site/src/icons/oauth.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								site/src/icons/oauth.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| import type { SVGProps } from "react"; | ||||
|  | ||||
| export function OAuthIcon(props: SVGProps<SVGSVGElement>) { | ||||
|   return ( | ||||
|     <svg | ||||
|       xmlns="http://www.w3.org/2000/svg" | ||||
|       width={24} | ||||
|       height={24} | ||||
|       viewBox="0 0 24 24" | ||||
|       {...props} | ||||
|     > | ||||
|       <g | ||||
|         fill="none" | ||||
|         stroke="currentColor" | ||||
|         strokeLinecap="round" | ||||
|         strokeLinejoin="round" | ||||
|         strokeWidth={2} | ||||
|       > | ||||
|         <path d="M2 12a10 10 0 1 0 20 0a10 10 0 1 0-20 0"></path> | ||||
|         <path d="M12.556 6c.65 0 1.235.373 1.508.947l2.839 7.848a1.646 1.646 0 0 1-1.01 2.108a1.673 1.673 0 0 1-2.068-.851L13.365 15h-2.73l-.398.905A1.67 1.67 0 0 1 8.26 16.95l-.153-.047a1.647 1.647 0 0 1-1.056-1.956l2.824-7.852a1.66 1.66 0 0 1 1.409-1.087z"></path> | ||||
|       </g> | ||||
|     </svg> | ||||
|   ); | ||||
| } | ||||
| @@ -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> | ||||
|   | ||||
| @@ -9,12 +9,16 @@ export const ContinuePage = () => { | ||||
|   const params = new URLSearchParams(queryString); | ||||
|   const redirectUri = params.get("redirect_uri"); | ||||
|  | ||||
|   const { isLoggedIn } = useUserContext(); | ||||
|   const { isLoggedIn, disableContinue } = useUserContext(); | ||||
|  | ||||
|   if (!isLoggedIn) { | ||||
|     return <Navigate to="/login" />; | ||||
|   } | ||||
|  | ||||
|   if (disableContinue && redirectUri !== "null") { | ||||
|     window.location.replace(redirectUri!); | ||||
|   } | ||||
|  | ||||
|   const redirect = () => { | ||||
|     notifications.show({ | ||||
|       title: "Redirecting", | ||||
|   | ||||
							
								
								
									
										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> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,4 +1,13 @@ | ||||
| import { Button, Paper, PasswordInput, TextInput, Title } from "@mantine/core"; | ||||
| import { | ||||
|   Button, | ||||
|   Paper, | ||||
|   PasswordInput, | ||||
|   TextInput, | ||||
|   Title, | ||||
|   Text, | ||||
|   Divider, | ||||
|   Grid, | ||||
| } from "@mantine/core"; | ||||
| import { useForm, zodResolver } from "@mantine/form"; | ||||
| import { notifications } from "@mantine/notifications"; | ||||
| import { useMutation } from "@tanstack/react-query"; | ||||
| @@ -7,20 +16,23 @@ import { z } from "zod"; | ||||
| import { useUserContext } from "../context/user-context"; | ||||
| import { Navigate } from "react-router"; | ||||
| import { Layout } from "../components/layouts/layout"; | ||||
| import { GoogleIcon } from "../icons/google"; | ||||
| import { GithubIcon } from "../icons/github"; | ||||
| import { OAuthIcon } from "../icons/oauth"; | ||||
|  | ||||
| export const LoginPage = () => { | ||||
|   const queryString = window.location.search; | ||||
|   const params = new URLSearchParams(queryString); | ||||
|   const redirectUri = params.get("redirect_uri"); | ||||
|  | ||||
|   const { isLoggedIn } = useUserContext(); | ||||
|   const { isLoggedIn, configuredProviders } = useUserContext(); | ||||
|  | ||||
|   if (isLoggedIn) { | ||||
|     return <Navigate to="/logout" />; | ||||
|   } | ||||
|  | ||||
|   const schema = z.object({ | ||||
|     username: z.string(), | ||||
|     email: z.string().email(), | ||||
|     password: z.string(), | ||||
|   }); | ||||
|  | ||||
| @@ -29,7 +41,7 @@ export const LoginPage = () => { | ||||
|   const form = useForm({ | ||||
|     mode: "uncontrolled", | ||||
|     initialValues: { | ||||
|       username: "", | ||||
|       email: "", | ||||
|       password: "", | ||||
|     }, | ||||
|     validate: zodResolver(schema), | ||||
| @@ -42,7 +54,7 @@ export const LoginPage = () => { | ||||
|     onError: () => { | ||||
|       notifications.show({ | ||||
|         title: "Failed to login", | ||||
|         message: "Check your username and password", | ||||
|         message: "Check your email and password", | ||||
|         color: "red", | ||||
|       }); | ||||
|     }, | ||||
| @@ -58,22 +70,104 @@ export const LoginPage = () => { | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   const loginOAuthMutation = useMutation({ | ||||
|     mutationFn: (provider: string) => { | ||||
|       return axios.get( | ||||
|         `/api/oauth/url/${provider}?redirect_uri=${redirectUri}`, | ||||
|       ); | ||||
|     }, | ||||
|     onError: () => { | ||||
|       notifications.show({ | ||||
|         title: "Internal error", | ||||
|         message: "Failed to get OAuth URL", | ||||
|         color: "red", | ||||
|       }); | ||||
|     }, | ||||
|     onSuccess: (data) => { | ||||
|       window.location.replace(data.data.url); | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   const handleSubmit = (values: FormValues) => { | ||||
|     loginMutation.mutate(values); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Layout> | ||||
|       <Title ta="center">Welcome back!</Title> | ||||
|       <Paper shadow="md" p={30} mt={30} radius="md" withBorder> | ||||
|       <Title ta="center">Tinyauth</Title> | ||||
|       <Paper shadow="md" p="xl" mt={30} radius="md" withBorder> | ||||
|         {configuredProviders.length === 0 && ( | ||||
|           <Text size="lg" mb="md" fw={500} ta="center"> | ||||
|             Welcome back, please login | ||||
|           </Text> | ||||
|         )} | ||||
|         {configuredProviders.length > 0 && ( | ||||
|           <> | ||||
|             <Text size="lg" fw={500} ta="center"> | ||||
|               Welcome back, login with | ||||
|             </Text> | ||||
|             <Grid mb="md" mt="md" align="center" justify="center"> | ||||
|               {configuredProviders.includes("google") && ( | ||||
|                 <Grid.Col span="content"> | ||||
|                   <Button | ||||
|                     radius="xl" | ||||
|                     leftSection={ | ||||
|                       <GoogleIcon style={{ width: 14, height: 14 }} /> | ||||
|                     } | ||||
|                     variant="default" | ||||
|                     onClick={() => loginOAuthMutation.mutate("google")} | ||||
|                     loading={loginOAuthMutation.isLoading} | ||||
|                   > | ||||
|                     Google | ||||
|                   </Button> | ||||
|                 </Grid.Col> | ||||
|               )} | ||||
|               {configuredProviders.includes("github") && ( | ||||
|                 <Grid.Col span="content"> | ||||
|                   <Button | ||||
|                     radius="xl" | ||||
|                     leftSection={ | ||||
|                       <GithubIcon style={{ width: 14, height: 14 }} /> | ||||
|                     } | ||||
|                     variant="default" | ||||
|                     onClick={() => loginOAuthMutation.mutate("github")} | ||||
|                     loading={loginOAuthMutation.isLoading} | ||||
|                   > | ||||
|                     Github | ||||
|                   </Button> | ||||
|                 </Grid.Col> | ||||
|               )} | ||||
|               {configuredProviders.includes("generic") && ( | ||||
|                 <Grid.Col span="content"> | ||||
|                   <Button | ||||
|                     radius="xl" | ||||
|                     leftSection={ | ||||
|                       <OAuthIcon style={{ width: 14, height: 14 }} /> | ||||
|                     } | ||||
|                     variant="default" | ||||
|                     onClick={() => loginOAuthMutation.mutate("generic")} | ||||
|                     loading={loginOAuthMutation.isLoading} | ||||
|                   > | ||||
|                     Generic | ||||
|                   </Button> | ||||
|                 </Grid.Col> | ||||
|               )} | ||||
|             </Grid> | ||||
|             <Divider | ||||
|               label="Or continue with email" | ||||
|               labelPosition="center" | ||||
|               my="lg" | ||||
|             /> | ||||
|           </> | ||||
|         )} | ||||
|         <form onSubmit={form.onSubmit(handleSubmit)}> | ||||
|           <TextInput | ||||
|             label="Username" | ||||
|             placeholder="tinyauth" | ||||
|             label="Email" | ||||
|             placeholder="user@example.com" | ||||
|             required | ||||
|             disabled={loginMutation.isLoading} | ||||
|             key={form.key("username")} | ||||
|             {...form.getInputProps("username")} | ||||
|             key={form.key("email")} | ||||
|             {...form.getInputProps("email")} | ||||
|           /> | ||||
|           <PasswordInput | ||||
|             label="Password" | ||||
| @@ -90,7 +184,7 @@ export const LoginPage = () => { | ||||
|             type="submit" | ||||
|             loading={loginMutation.isLoading} | ||||
|           > | ||||
|             Sign in | ||||
|             Login | ||||
|           </Button> | ||||
|         </form> | ||||
|       </Paper> | ||||
|   | ||||
| @@ -5,9 +5,10 @@ import axios from "axios"; | ||||
| import { useUserContext } from "../context/user-context"; | ||||
| import { Navigate } from "react-router"; | ||||
| import { Layout } from "../components/layouts/layout"; | ||||
| import { capitalize } from "../utils/utils"; | ||||
|  | ||||
| export const LogoutPage = () => { | ||||
|   const { isLoggedIn, username } = useUserContext(); | ||||
|   const { isLoggedIn, email, oauth, provider } = useUserContext(); | ||||
|  | ||||
|   if (!isLoggedIn) { | ||||
|     return <Navigate to="/login" />; | ||||
| @@ -43,8 +44,9 @@ export const LogoutPage = () => { | ||||
|           Logout | ||||
|         </Text> | ||||
|         <Text> | ||||
|           You are currently logged in as <Code>{username}</Code>, click the | ||||
|           button below to log out. | ||||
|           You are currently logged in as <Code>{email}</Code> | ||||
|           {oauth && ` using ${capitalize(provider)}`}. Click the button below to | ||||
|           log out. | ||||
|         </Text> | ||||
|         <Button | ||||
|           fullWidth | ||||
|   | ||||
							
								
								
									
										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> | ||||
|   ); | ||||
| }; | ||||
| @@ -2,7 +2,11 @@ import { z } from "zod"; | ||||
|  | ||||
| export const userContextSchema = z.object({ | ||||
|   isLoggedIn: z.boolean(), | ||||
|   username: z.string(), | ||||
|   email: z.string(), | ||||
|   oauth: z.boolean(), | ||||
|   provider: z.string(), | ||||
|   configuredProviders: z.array(z.string()), | ||||
|   disableContinue: z.boolean(), | ||||
| }); | ||||
|  | ||||
| export type UserContextSchemaType = z.infer<typeof userContextSchema>; | ||||
|   | ||||
							
								
								
									
										1
									
								
								site/src/utils/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								site/src/utils/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); | ||||
		Reference in New Issue
	
	Block a user