mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-31 14:15:50 +00:00 
			
		
		
		
	Compare commits
	
		
			13 Commits
		
	
	
		
			feat/analy
			...
			dad0718091
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | dad0718091 | ||
|   | d4069900bc | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a54996d72d | ||
|   | 085f6257c5 | ||
|   | c307f7eb2e | ||
|   | 5dd8526833 | ||
|   | e8558b89b4 | ||
|   | f8047a6c2e | ||
|   | e114bf0943 | ||
|   | c9867ccb76 | ||
|   | 866933b3d6 | ||
|   | d70cbea546 | ||
|   | 50105e4e9d | 
							
								
								
									
										19
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								.env.example
									
									
									
									
									
								
							| @@ -4,20 +4,6 @@ APP_URL=http://localhost:3000 | |||||||
| USERS=your_user_password_hash | USERS=your_user_password_hash | ||||||
| USERS_FILE=users_file | USERS_FILE=users_file | ||||||
| SECURE_COOKIE=false | SECURE_COOKIE=false | ||||||
| GITHUB_CLIENT_ID=github_client_id |  | ||||||
| GITHUB_CLIENT_SECRET=github_client_secret |  | ||||||
| GITHUB_CLIENT_SECRET_FILE=github_client_secret_file |  | ||||||
| GOOGLE_CLIENT_ID=google_client_id |  | ||||||
| GOOGLE_CLIENT_SECRET=google_client_secret |  | ||||||
| GOOGLE_CLIENT_SECRET_FILE=google_client_secret_file |  | ||||||
| GENERIC_CLIENT_ID=generic_client_id |  | ||||||
| GENERIC_CLIENT_SECRET=generic_client_secret |  | ||||||
| GENERIC_CLIENT_SECRET_FILE=generic_client_secret_file |  | ||||||
| GENERIC_SCOPES=generic_scopes |  | ||||||
| GENERIC_AUTH_URL=generic_auth_url |  | ||||||
| GENERIC_TOKEN_URL=generic_token_url |  | ||||||
| GENERIC_USER_URL=generic_user_url |  | ||||||
| DISABLE_CONTINUE=false |  | ||||||
| OAUTH_WHITELIST= | OAUTH_WHITELIST= | ||||||
| GENERIC_NAME=My OAuth | GENERIC_NAME=My OAuth | ||||||
| SESSION_EXPIRY=7200 | SESSION_EXPIRY=7200 | ||||||
| @@ -30,4 +16,7 @@ OAUTH_AUTO_REDIRECT=none | |||||||
| BACKGROUND_IMAGE=some_image_url | BACKGROUND_IMAGE=some_image_url | ||||||
| GENERIC_SKIP_SSL=false | GENERIC_SKIP_SSL=false | ||||||
| RESOURCES_DIR=/data/resources | RESOURCES_DIR=/data/resources | ||||||
| DATABASE_PATH=/data/tinyauth.db | DATABASE_PATH=/data/tinyauth.db | ||||||
|  | DISABLE_ANALYTICS=false | ||||||
|  | DISABLE_RESOURCES=false | ||||||
|  | TRUSTED_PROXIES= | ||||||
							
								
								
									
										4
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							| @@ -80,7 +80,7 @@ jobs: | |||||||
|       - name: Build |       - name: Build | ||||||
|         run: | |         run: | | ||||||
|           cp -r frontend/dist internal/assets/dist |           cp -r frontend/dist internal/assets/dist | ||||||
|           go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 |           go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 | ||||||
|         env: |         env: | ||||||
|           CGO_ENABLED: 0 |           CGO_ENABLED: 0 | ||||||
|  |  | ||||||
| @@ -126,7 +126,7 @@ jobs: | |||||||
|       - name: Build |       - name: Build | ||||||
|         run: | |         run: | | ||||||
|           cp -r frontend/dist internal/assets/dist |           cp -r frontend/dist internal/assets/dist | ||||||
|           go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 |           go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 | ||||||
|         env: |         env: | ||||||
|           CGO_ENABLED: 0 |           CGO_ENABLED: 0 | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -58,7 +58,7 @@ jobs: | |||||||
|       - name: Build |       - name: Build | ||||||
|         run: | |         run: | | ||||||
|           cp -r frontend/dist internal/assets/dist |           cp -r frontend/dist internal/assets/dist | ||||||
|           go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 |           go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-amd64 | ||||||
|         env: |         env: | ||||||
|           CGO_ENABLED: 0 |           CGO_ENABLED: 0 | ||||||
|  |  | ||||||
| @@ -101,7 +101,7 @@ jobs: | |||||||
|       - name: Build |       - name: Build | ||||||
|         run: | |         run: | | ||||||
|           cp -r frontend/dist internal/assets/dist |           cp -r frontend/dist internal/assets/dist | ||||||
|           go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/constants.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/constants.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 |           go build -ldflags "-s -w -X tinyauth/internal/config.Version=${{ needs.generate-metadata.outputs.VERSION }} -X tinyauth/internal/config.CommitHash=${{ needs.generate-metadata.outputs.COMMIT_HASH }} -X tinyauth/internal/config.BuildTimestamp=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }}" -o tinyauth-arm64 | ||||||
|         env: |         env: | ||||||
|           CGO_ENABLED: 0 |           CGO_ENABLED: 0 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| # Site builder | # Site builder | ||||||
| FROM oven/bun:1.2.22-alpine AS frontend-builder | FROM oven/bun:1.2.23-alpine AS frontend-builder | ||||||
|  |  | ||||||
| WORKDIR /frontend | WORKDIR /frontend | ||||||
|  |  | ||||||
| @@ -38,7 +38,7 @@ COPY ./cmd ./cmd | |||||||
| COPY ./internal ./internal | COPY ./internal ./internal | ||||||
| COPY --from=frontend-builder /frontend/dist ./internal/assets/dist | COPY --from=frontend-builder /frontend/dist ./internal/assets/dist | ||||||
|  |  | ||||||
| RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/constants.Version=${VERSION} -X tinyauth/internal/constants.CommitHash=${COMMIT_HASH} -X tinyauth/internal/constants.BuildTimestamp=${BUILD_TIMESTAMP}"  | RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}"  | ||||||
|   |   | ||||||
| # Runner | # Runner | ||||||
| FROM alpine:3.22 AS runner | FROM alpine:3.22 AS runner | ||||||
|   | |||||||
| @@ -94,6 +94,8 @@ func init() { | |||||||
| 		{"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."}, | 		{"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."}, | ||||||
| 		{"database-path", "/data/tinyauth.db", "Path to the Sqlite database file."}, | 		{"database-path", "/data/tinyauth.db", "Path to the Sqlite database file."}, | ||||||
| 		{"trusted-proxies", "", "Comma separated list of trusted proxies (IP addresses or CIDRs) for correct client IP detection."}, | 		{"trusted-proxies", "", "Comma separated list of trusted proxies (IP addresses or CIDRs) for correct client IP detection."}, | ||||||
|  | 		{"disable-analytics", false, "Disable anonymous version collection."}, | ||||||
|  | 		{"disable-resources", false, "Disable the resources server."}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, opt := range configOptions { | 	for _, opt := range configOptions { | ||||||
|   | |||||||
| @@ -70,7 +70,7 @@ var VerifyCmd = &cobra.Command{ | |||||||
|  |  | ||||||
| 		err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(iPassword)) | 		err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(iPassword)) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal().Msg("Ppassword is incorrect") | 			log.Fatal().Msg("Password is incorrect") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if user.TotpSecret == "" { | 		if user.TotpSecret == "" { | ||||||
|   | |||||||
| @@ -34,6 +34,10 @@ services: | |||||||
|     build: |     build: | ||||||
|       context: . |       context: . | ||||||
|       dockerfile: Dockerfile.dev |       dockerfile: Dockerfile.dev | ||||||
|  |       args: | ||||||
|  |         - VERSION=development | ||||||
|  |         - COMMIT_HASH=development | ||||||
|  |         - BUILD_TIMESTAMP=000-00-00T00:00:00Z | ||||||
|     env_file: .env |     env_file: .env | ||||||
|     volumes: |     volumes: | ||||||
|       - ./internal:/tinyauth/internal |       - ./internal:/tinyauth/internal | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ | |||||||
|     <link rel="shortcut icon" href="/favicon.ico" /> |     <link rel="shortcut icon" href="/favicon.ico" /> | ||||||
|     <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> |     <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> | ||||||
|     <meta name="apple-mobile-web-app-title" content="Tinyauth" /> |     <meta name="apple-mobile-web-app-title" content="Tinyauth" /> | ||||||
|  |     <meta name="robots" content="noindex" /> | ||||||
|     <link rel="manifest" href="/site.webmanifest" /> |     <link rel="manifest" href="/site.webmanifest" /> | ||||||
|     <title>Tinyauth</title> |     <title>Tinyauth</title> | ||||||
|   </head> |   </head> | ||||||
|   | |||||||
| @@ -44,6 +44,7 @@ export const TotpForm = (props: Props) => { | |||||||
|                   disabled={loading} |                   disabled={loading} | ||||||
|                   {...field} |                   {...field} | ||||||
|                   autoComplete="one-time-code" |                   autoComplete="one-time-code" | ||||||
|  |                   autoFocus | ||||||
|                 > |                 > | ||||||
|                   <InputOTPGroup> |                   <InputOTPGroup> | ||||||
|                     <InputOTPSlot index={0} /> |                     <InputOTPSlot index={0} /> | ||||||
|   | |||||||
| @@ -1,10 +1,14 @@ | |||||||
| package bootstrap | package bootstrap | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"os" | 	"os" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"time" | ||||||
| 	"tinyauth/internal/config" | 	"tinyauth/internal/config" | ||||||
| 	"tinyauth/internal/controller" | 	"tinyauth/internal/controller" | ||||||
| 	"tinyauth/internal/middleware" | 	"tinyauth/internal/middleware" | ||||||
| @@ -29,40 +33,43 @@ type Service interface { | |||||||
| } | } | ||||||
|  |  | ||||||
| type BootstrapApp struct { | type BootstrapApp struct { | ||||||
| 	Config config.Config | 	config config.Config | ||||||
|  | 	uuid   string | ||||||
| } | } | ||||||
|  |  | ||||||
| func NewBootstrapApp(config config.Config) *BootstrapApp { | func NewBootstrapApp(config config.Config) *BootstrapApp { | ||||||
| 	return &BootstrapApp{ | 	return &BootstrapApp{ | ||||||
| 		Config: config, | 		config: config, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (app *BootstrapApp) Setup() error { | func (app *BootstrapApp) Setup() error { | ||||||
| 	// Parse users | 	// Parse users | ||||||
| 	users, err := utils.GetUsers(app.Config.Users, app.Config.UsersFile) | 	users, err := utils.GetUsers(app.config.Users, app.config.UsersFile) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Get OAuth configs | 	// Get OAuth configs | ||||||
| 	oauthProviders, err := utils.GetOAuthProvidersConfig(os.Environ(), os.Args, app.Config.AppURL) | 	oauthProviders, err := utils.GetOAuthProvidersConfig(os.Environ(), os.Args, app.config.AppURL) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Get cookie domain | 	// Get cookie domain | ||||||
| 	cookieDomain, err := utils.GetCookieDomain(app.Config.AppURL) | 	cookieDomain, err := utils.GetCookieDomain(app.config.AppURL) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Cookie names | 	// Cookie names | ||||||
| 	appUrl, _ := url.Parse(app.Config.AppURL) // Already validated | 	appUrl, _ := url.Parse(app.config.AppURL) // Already validated | ||||||
| 	cookieId := utils.GenerateIdentifier(appUrl.Hostname()) | 	uuid := utils.GenerateUUID(appUrl.Hostname()) | ||||||
|  | 	app.uuid = uuid | ||||||
|  | 	cookieId := strings.Split(uuid, "-")[0] | ||||||
| 	sessionCookieName := fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId) | 	sessionCookieName := fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId) | ||||||
| 	csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId) | 	csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId) | ||||||
| 	redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId) | 	redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId) | ||||||
| @@ -70,26 +77,26 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 	// Create configs | 	// Create configs | ||||||
| 	authConfig := service.AuthServiceConfig{ | 	authConfig := service.AuthServiceConfig{ | ||||||
| 		Users:             users, | 		Users:             users, | ||||||
| 		OauthWhitelist:    app.Config.OAuthWhitelist, | 		OauthWhitelist:    app.config.OAuthWhitelist, | ||||||
| 		SessionExpiry:     app.Config.SessionExpiry, | 		SessionExpiry:     app.config.SessionExpiry, | ||||||
| 		SecureCookie:      app.Config.SecureCookie, | 		SecureCookie:      app.config.SecureCookie, | ||||||
| 		CookieDomain:      cookieDomain, | 		CookieDomain:      cookieDomain, | ||||||
| 		LoginTimeout:      app.Config.LoginTimeout, | 		LoginTimeout:      app.config.LoginTimeout, | ||||||
| 		LoginMaxRetries:   app.Config.LoginMaxRetries, | 		LoginMaxRetries:   app.config.LoginMaxRetries, | ||||||
| 		SessionCookieName: sessionCookieName, | 		SessionCookieName: sessionCookieName, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Setup services | 	// Setup services | ||||||
| 	var ldapService *service.LdapService | 	var ldapService *service.LdapService | ||||||
|  |  | ||||||
| 	if app.Config.LdapAddress != "" { | 	if app.config.LdapAddress != "" { | ||||||
| 		ldapConfig := service.LdapServiceConfig{ | 		ldapConfig := service.LdapServiceConfig{ | ||||||
| 			Address:      app.Config.LdapAddress, | 			Address:      app.config.LdapAddress, | ||||||
| 			BindDN:       app.Config.LdapBindDN, | 			BindDN:       app.config.LdapBindDN, | ||||||
| 			BindPassword: app.Config.LdapBindPassword, | 			BindPassword: app.config.LdapBindPassword, | ||||||
| 			BaseDN:       app.Config.LdapBaseDN, | 			BaseDN:       app.config.LdapBaseDN, | ||||||
| 			Insecure:     app.Config.LdapInsecure, | 			Insecure:     app.config.LdapInsecure, | ||||||
| 			SearchFilter: app.Config.LdapSearchFilter, | 			SearchFilter: app.config.LdapSearchFilter, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		ldapService = service.NewLdapService(ldapConfig) | 		ldapService = service.NewLdapService(ldapConfig) | ||||||
| @@ -104,7 +111,7 @@ func (app *BootstrapApp) Setup() error { | |||||||
|  |  | ||||||
| 	// Bootstrap database | 	// Bootstrap database | ||||||
| 	databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{ | 	databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{ | ||||||
| 		DatabasePath: app.Config.DatabasePath, | 		DatabasePath: app.config.DatabasePath, | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	log.Debug().Str("service", fmt.Sprintf("%T", databaseService)).Msg("Initializing service") | 	log.Debug().Str("service", fmt.Sprintf("%T", databaseService)).Msg("Initializing service") | ||||||
| @@ -140,10 +147,6 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Configured providers | 	// Configured providers | ||||||
| 	babysit := map[string]string{ |  | ||||||
| 		"google": "Google", |  | ||||||
| 		"github": "GitHub", |  | ||||||
| 	} |  | ||||||
| 	configuredProviders := make([]controller.Provider, 0) | 	configuredProviders := make([]controller.Provider, 0) | ||||||
|  |  | ||||||
| 	for id, provider := range oauthProviders { | 	for id, provider := range oauthProviders { | ||||||
| @@ -152,7 +155,7 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if provider.Name == "" { | 		if provider.Name == "" { | ||||||
| 			if name, ok := babysit[id]; ok { | 			if name, ok := config.OverrideProviders[id]; ok { | ||||||
| 				provider.Name = name | 				provider.Name = name | ||||||
| 			} else { | 			} else { | ||||||
| 				provider.Name = utils.Capitalize(id) | 				provider.Name = utils.Capitalize(id) | ||||||
| @@ -181,16 +184,20 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Create engine | 	// Create engine | ||||||
| 	engine := gin.New() |  | ||||||
|  |  | ||||||
| 	if len(app.Config.TrustedProxies) > 0 { |  | ||||||
| 		engine.SetTrustedProxies(strings.Split(app.Config.TrustedProxies, ",")) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if config.Version != "development" { | 	if config.Version != "development" { | ||||||
| 		gin.SetMode(gin.ReleaseMode) | 		gin.SetMode(gin.ReleaseMode) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	engine := gin.New() | ||||||
|  |  | ||||||
|  | 	if len(app.config.TrustedProxies) > 0 { | ||||||
|  | 		err := engine.SetTrustedProxies(strings.Split(app.config.TrustedProxies, ",")) | ||||||
|  |  | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("failed to set trusted proxies: %w", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Create middlewares | 	// Create middlewares | ||||||
| 	var middlewares []Middleware | 	var middlewares []Middleware | ||||||
|  |  | ||||||
| @@ -219,24 +226,24 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 	// Create controllers | 	// Create controllers | ||||||
| 	contextController := controller.NewContextController(controller.ContextControllerConfig{ | 	contextController := controller.NewContextController(controller.ContextControllerConfig{ | ||||||
| 		Providers:             configuredProviders, | 		Providers:             configuredProviders, | ||||||
| 		Title:                 app.Config.Title, | 		Title:                 app.config.Title, | ||||||
| 		AppURL:                app.Config.AppURL, | 		AppURL:                app.config.AppURL, | ||||||
| 		CookieDomain:          cookieDomain, | 		CookieDomain:          cookieDomain, | ||||||
| 		ForgotPasswordMessage: app.Config.ForgotPasswordMessage, | 		ForgotPasswordMessage: app.config.ForgotPasswordMessage, | ||||||
| 		BackgroundImage:       app.Config.BackgroundImage, | 		BackgroundImage:       app.config.BackgroundImage, | ||||||
| 		OAuthAutoRedirect:     app.Config.OAuthAutoRedirect, | 		OAuthAutoRedirect:     app.config.OAuthAutoRedirect, | ||||||
| 	}, apiRouter) | 	}, apiRouter) | ||||||
|  |  | ||||||
| 	oauthController := controller.NewOAuthController(controller.OAuthControllerConfig{ | 	oauthController := controller.NewOAuthController(controller.OAuthControllerConfig{ | ||||||
| 		AppURL:             app.Config.AppURL, | 		AppURL:             app.config.AppURL, | ||||||
| 		SecureCookie:       app.Config.SecureCookie, | 		SecureCookie:       app.config.SecureCookie, | ||||||
| 		CSRFCookieName:     csrfCookieName, | 		CSRFCookieName:     csrfCookieName, | ||||||
| 		RedirectCookieName: redirectCookieName, | 		RedirectCookieName: redirectCookieName, | ||||||
| 		CookieDomain:       cookieDomain, | 		CookieDomain:       cookieDomain, | ||||||
| 	}, apiRouter, authService, oauthBrokerService) | 	}, apiRouter, authService, oauthBrokerService) | ||||||
|  |  | ||||||
| 	proxyController := controller.NewProxyController(controller.ProxyControllerConfig{ | 	proxyController := controller.NewProxyController(controller.ProxyControllerConfig{ | ||||||
| 		AppURL: app.Config.AppURL, | 		AppURL: app.config.AppURL, | ||||||
| 	}, apiRouter, dockerService, authService) | 	}, apiRouter, dockerService, authService) | ||||||
|  |  | ||||||
| 	userController := controller.NewUserController(controller.UserControllerConfig{ | 	userController := controller.NewUserController(controller.UserControllerConfig{ | ||||||
| @@ -244,7 +251,8 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 	}, apiRouter, authService) | 	}, apiRouter, authService) | ||||||
|  |  | ||||||
| 	resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{ | 	resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{ | ||||||
| 		ResourcesDir: app.Config.ResourcesDir, | 		ResourcesDir:      app.config.ResourcesDir, | ||||||
|  | 		ResourcesDisabled: app.config.DisableResources, | ||||||
| 	}, mainRouter) | 	}, mainRouter) | ||||||
|  |  | ||||||
| 	healthController := controller.NewHealthController(apiRouter) | 	healthController := controller.NewHealthController(apiRouter) | ||||||
| @@ -264,8 +272,14 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 		ctrl.SetupRoutes() | 		ctrl.SetupRoutes() | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// If analytics are not disabled, start heartbeat | ||||||
|  | 	if !app.config.DisableAnalytics { | ||||||
|  | 		log.Debug().Msg("Starting heartbeat routine") | ||||||
|  | 		go app.heartbeat() | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Start server | 	// Start server | ||||||
| 	address := fmt.Sprintf("%s:%d", app.Config.Address, app.Config.Port) | 	address := fmt.Sprintf("%s:%d", app.config.Address, app.config.Port) | ||||||
| 	log.Info().Msgf("Starting server on %s", address) | 	log.Info().Msgf("Starting server on %s", address) | ||||||
| 	if err := engine.Run(address); err != nil { | 	if err := engine.Run(address); err != nil { | ||||||
| 		log.Fatal().Err(err).Msg("Failed to start server") | 		log.Fatal().Err(err).Msg("Failed to start server") | ||||||
| @@ -273,3 +287,55 @@ func (app *BootstrapApp) Setup() error { | |||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (app *BootstrapApp) heartbeat() { | ||||||
|  | 	ticker := time.NewTicker(time.Duration(12) * time.Hour) | ||||||
|  | 	defer ticker.Stop() | ||||||
|  |  | ||||||
|  | 	type heartbeat struct { | ||||||
|  | 		UUID    string `json:"uuid"` | ||||||
|  | 		Version string `json:"version"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var body heartbeat | ||||||
|  |  | ||||||
|  | 	body.UUID = app.uuid | ||||||
|  | 	body.Version = config.Version | ||||||
|  |  | ||||||
|  | 	bodyJson, err := json.Marshal(body) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error().Err(err).Msg("Failed to marshal heartbeat body") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	client := &http.Client{} | ||||||
|  |  | ||||||
|  | 	heartbeatURL := config.ApiServer + "/v1/instances/heartbeat" | ||||||
|  |  | ||||||
|  | 	for ; true; <-ticker.C { | ||||||
|  | 		log.Debug().Msg("Sending heartbeat") | ||||||
|  |  | ||||||
|  | 		req, err := http.NewRequest(http.MethodPost, heartbeatURL, bytes.NewReader(bodyJson)) | ||||||
|  |  | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error().Err(err).Msg("Failed to create heartbeat request") | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		req.Header.Add("Content-Type", "application/json") | ||||||
|  |  | ||||||
|  | 		res, err := client.Do(req) | ||||||
|  |  | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error().Err(err).Msg("Failed to send heartbeat") | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		res.Body.Close() | ||||||
|  |  | ||||||
|  | 		if res.StatusCode != 200 && res.StatusCode != 201 { | ||||||
|  | 			log.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -3,8 +3,8 @@ package config | |||||||
| // Version information, set at build time | // Version information, set at build time | ||||||
|  |  | ||||||
| var Version = "development" | var Version = "development" | ||||||
| var CommitHash = "n/a" | var CommitHash = "development" | ||||||
| var BuildTimestamp = "n/a" | var BuildTimestamp = "0000-00-00T00:00:00Z" | ||||||
|  |  | ||||||
| // Cookie name templates | // Cookie name templates | ||||||
|  |  | ||||||
| @@ -39,6 +39,8 @@ type Config struct { | |||||||
| 	ResourcesDir          string `mapstructure:"resources-dir"` | 	ResourcesDir          string `mapstructure:"resources-dir"` | ||||||
| 	DatabasePath          string `mapstructure:"database-path" validate:"required"` | 	DatabasePath          string `mapstructure:"database-path" validate:"required"` | ||||||
| 	TrustedProxies        string `mapstructure:"trusted-proxies"` | 	TrustedProxies        string `mapstructure:"trusted-proxies"` | ||||||
|  | 	DisableAnalytics      bool   `mapstructure:"disable-analytics"` | ||||||
|  | 	DisableResources      bool   `mapstructure:"disable-resources"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // OAuth/OIDC config | // OAuth/OIDC config | ||||||
| @@ -63,6 +65,11 @@ type OAuthServiceConfig struct { | |||||||
| 	Name               string   `key:"name"` | 	Name               string   `key:"name"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | var OverrideProviders = map[string]string{ | ||||||
|  | 	"google": "Google", | ||||||
|  | 	"github": "GitHub", | ||||||
|  | } | ||||||
|  |  | ||||||
| // User/session related stuff | // User/session related stuff | ||||||
|  |  | ||||||
| type User struct { | type User struct { | ||||||
| @@ -169,3 +176,7 @@ type AppPath struct { | |||||||
| type Providers struct { | type Providers struct { | ||||||
| 	Providers map[string]OAuthServiceConfig | 	Providers map[string]OAuthServiceConfig | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // API server | ||||||
|  |  | ||||||
|  | var ApiServer = "https://api.tinyauth.app" | ||||||
|   | |||||||
| @@ -7,7 +7,8 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type ResourcesControllerConfig struct { | type ResourcesControllerConfig struct { | ||||||
| 	ResourcesDir string | 	ResourcesDir      string | ||||||
|  | 	ResourcesDisabled bool | ||||||
| } | } | ||||||
|  |  | ||||||
| type ResourcesController struct { | type ResourcesController struct { | ||||||
| @@ -38,5 +39,12 @@ func (controller *ResourcesController) resourcesHandler(c *gin.Context) { | |||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 	if controller.config.ResourcesDisabled { | ||||||
|  | 		c.JSON(403, gin.H{ | ||||||
|  | 			"status":  403, | ||||||
|  | 			"message": "Resources are disabled", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
| 	controller.fileServer.ServeHTTP(c.Writer, c.Request) | 	controller.fileServer.ServeHTTP(c.Writer, c.Request) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -309,12 +309,14 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte | |||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if context.Provider != "generic" { | 	for id := range config.OverrideProviders { | ||||||
| 		log.Debug().Msg("Not using generic provider, skipping group check") | 		if context.Provider == id { | ||||||
| 		return true | 			log.Info().Str("provider", id).Msg("OAuth groups not supported for this provider") | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, userGroup := range strings.Split(context.OAuthGroups, ",") { | 	for userGroup := range strings.SplitSeq(context.OAuthGroups, ",") { | ||||||
| 		if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) { | 		if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) { | ||||||
| 			return true | 			return true | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -50,7 +50,7 @@ func (broker *OAuthBrokerService) Init() error { | |||||||
| 			log.Error().Err(err).Msgf("Failed to initialize OAuth service: %T", name) | 			log.Error().Err(err).Msgf("Failed to initialize OAuth service: %T", name) | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		log.Info().Msgf("Initialized OAuth service: %T", name) | 		log.Info().Str("service", service.GetName()).Msg("Initialized OAuth service") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
|   | |||||||
| @@ -183,14 +183,13 @@ func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[st | |||||||
| 		providers[name] = provider | 		providers[name] = provider | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If we have google/github providers and no redirect URL babysit them | 	// If we have google/github providers and no redirect URL then set a default | ||||||
| 	babysitProviders := []string{"google", "github"} |  | ||||||
|  |  | ||||||
| 	for _, name := range babysitProviders { | 	for id := range config.OverrideProviders { | ||||||
| 		if provider, exists := providers[name]; exists { | 		if provider, exists := providers[id]; exists { | ||||||
| 			if provider.RedirectURL == "" { | 			if provider.RedirectURL == "" { | ||||||
| 				provider.RedirectURL = appUrl + "/api/oauth/callback/" + name | 				provider.RedirectURL = appUrl + "/api/oauth/callback/" + id | ||||||
| 				providers[name] = provider | 				providers[id] = provider | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -18,10 +18,14 @@ func NormalizeKeys(keys map[string]string, rootName string, sep string) map[stri | |||||||
|  |  | ||||||
| 		finalKey = append(finalKey, rootName) | 		finalKey = append(finalKey, rootName) | ||||||
| 		finalKey = append(finalKey, "providers") | 		finalKey = append(finalKey, "providers") | ||||||
| 		cebabKey := strings.ToLower(k) | 		lowerKey := strings.ToLower(k) | ||||||
|  |  | ||||||
|  | 		if !strings.HasPrefix(lowerKey, "providers"+sep) { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		for _, known := range knownKeys { | 		for _, known := range knownKeys { | ||||||
| 			if strings.HasSuffix(cebabKey, strings.ReplaceAll(known, "-", sep)) { | 			if strings.HasSuffix(lowerKey, strings.ReplaceAll(known, "-", sep)) { | ||||||
| 				suffix = known | 				suffix = known | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
| @@ -31,7 +35,11 @@ func NormalizeKeys(keys map[string]string, rootName string, sep string) map[stri | |||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		clientNameParts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(cebabKey, sep+strings.ReplaceAll(suffix, "-", sep)), "providers"+sep), sep) | 		if strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(lowerKey, "providers"+sep), strings.ReplaceAll(suffix, "-", sep))) == "" { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		clientNameParts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(lowerKey, sep+strings.ReplaceAll(suffix, "-", sep)), "providers"+sep), sep) | ||||||
|  |  | ||||||
| 		for i, p := range clientNameParts { | 		for i, p := range clientNameParts { | ||||||
| 			if i == 0 { | 			if i == 0 { | ||||||
| @@ -46,9 +54,9 @@ func NormalizeKeys(keys map[string]string, rootName string, sep string) map[stri | |||||||
|  |  | ||||||
| 		finalKey = append(finalKey, camelClientName) | 		finalKey = append(finalKey, camelClientName) | ||||||
|  |  | ||||||
| 		filedParts := strings.Split(suffix, "-") | 		fieldParts := strings.Split(suffix, "-") | ||||||
|  |  | ||||||
| 		for i, p := range filedParts { | 		for i, p := range fieldParts { | ||||||
| 			if i == 0 { | 			if i == 0 { | ||||||
| 				camelField += p | 				camelField += p | ||||||
| 				continue | 				continue | ||||||
|   | |||||||
| @@ -14,6 +14,8 @@ func TestNormalizeKeys(t *testing.T) { | |||||||
| 		"PROVIDERS_CLIENT1_CLIENT_SECRET":                "my-client-secret", | 		"PROVIDERS_CLIENT1_CLIENT_SECRET":                "my-client-secret", | ||||||
| 		"PROVIDERS_MY_AWESOME_CLIENT_CLIENT_ID":          "my-awesome-client-id", | 		"PROVIDERS_MY_AWESOME_CLIENT_CLIENT_ID":          "my-awesome-client-id", | ||||||
| 		"PROVIDERS_MY_AWESOME_CLIENT_CLIENT_SECRET_FILE": "/path/to/secret", | 		"PROVIDERS_MY_AWESOME_CLIENT_CLIENT_SECRET_FILE": "/path/to/secret", | ||||||
|  | 		"I_LOOK_LIKE_A_KEY_CLIENT_ID":                    "should-not-appear", | ||||||
|  | 		"PROVIDERS_CLIENT_ID":                            "should-not-appear", | ||||||
| 	} | 	} | ||||||
| 	expected := map[string]string{ | 	expected := map[string]string{ | ||||||
| 		"tinyauth.providers.client1.clientId":                 "my-client-id", | 		"tinyauth.providers.client1.clientId":                 "my-client-id", | ||||||
| @@ -31,6 +33,9 @@ func TestNormalizeKeys(t *testing.T) { | |||||||
| 		"providers-client1-client-secret":                "my-client-secret", | 		"providers-client1-client-secret":                "my-client-secret", | ||||||
| 		"providers-my-awesome-client-client-id":          "my-awesome-client-id", | 		"providers-my-awesome-client-client-id":          "my-awesome-client-id", | ||||||
| 		"providers-my-awesome-client-client-secret-file": "/path/to/secret", | 		"providers-my-awesome-client-client-secret-file": "/path/to/secret", | ||||||
|  | 		"providers-should-not-appear-client":             "should-not-appear", | ||||||
|  | 		"i-look-like-a-key-client-id":                    "should-not-appear", | ||||||
|  | 		"providers-client-id":                            "should-not-appear", | ||||||
| 	} | 	} | ||||||
| 	expected = map[string]string{ | 	expected = map[string]string{ | ||||||
| 		"tinyauth.providers.client1.clientId":                 "my-client-id", | 		"tinyauth.providers.client1.clientId":                 "my-client-id", | ||||||
|   | |||||||
| @@ -101,8 +101,7 @@ func CheckFilter(filter string, str string) bool { | |||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
| func GenerateIdentifier(str string) string { | func GenerateUUID(str string) string { | ||||||
| 	uuid := uuid.NewSHA1(uuid.NameSpaceURL, []byte(str)) | 	uuid := uuid.NewSHA1(uuid.NameSpaceURL, []byte(str)) | ||||||
| 	uuidString := uuid.String() | 	return uuid.String() | ||||||
| 	return strings.Split(uuidString, "-")[0] |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -136,16 +136,13 @@ func TestCheckFilter(t *testing.T) { | |||||||
| 	assert.Equal(t, false, utils.CheckFilter("apple, banana, cherry", "grape")) | 	assert.Equal(t, false, utils.CheckFilter("apple, banana, cherry", "grape")) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestGenerateIdentifier(t *testing.T) { | func TestGenerateUUID(t *testing.T) { | ||||||
| 	// Consistent output for same input | 	// Consistent output for same input | ||||||
| 	id1 := utils.GenerateIdentifier("teststring") | 	id1 := utils.GenerateUUID("teststring") | ||||||
| 	id2 := utils.GenerateIdentifier("teststring") | 	id2 := utils.GenerateUUID("teststring") | ||||||
| 	assert.Equal(t, id1, id2) | 	assert.Equal(t, id1, id2) | ||||||
|  |  | ||||||
| 	// Different output for different input | 	// Different output for different input | ||||||
| 	id3 := utils.GenerateIdentifier("differentstring") | 	id3 := utils.GenerateUUID("differentstring") | ||||||
| 	assert.Assert(t, id1 != id3) | 	assert.Assert(t, id1 != id3) | ||||||
|  |  | ||||||
| 	// Check length (should be 8 characters from first segment of UUID) |  | ||||||
| 	assert.Equal(t, 8, len(id1)) |  | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user