Compare commits

...

13 Commits

Author SHA1 Message Date
Stavros
dad0718091 fix: disable search engine indexing 2025-10-05 16:41:15 +03:00
Stavros
d4069900bc feat: autofocus totp form 2025-10-03 16:36:10 +03:00
dependabot[bot]
a54996d72d chore(deps): bump oven/bun from 1.2.22-alpine to 1.2.23-alpine (#373)
Bumps oven/bun from 1.2.22-alpine to 1.2.23-alpine.

---
updated-dependencies:
- dependency-name: oven/bun
  dependency-version: 1.2.23-alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-03 16:32:17 +03:00
Stavros
085f6257c5 fix: fix oauth group provider check 2025-09-25 22:35:44 +03:00
Stavros
c307f7eb2e fix: handle 201 status for heartbeat 2025-09-22 19:56:59 +03:00
Stavros
5dd8526833 fix: fix key normalization function handing more cases than it needs to 2025-09-22 19:29:55 +03:00
Stavros
e8558b89b4 fix: set gin mode correctly 2025-09-22 16:15:55 +03:00
Stavros
f8047a6c2e feat: add option to disable resources server 2025-09-22 15:52:43 +03:00
axjp
e114bf0943 Update verify.go (#364) 2025-09-21 09:52:41 +03:00
Stavros
c9867ccb76 chore: fix typo 2025-09-20 11:08:57 +03:00
Stavros
866933b3d6 fix: fix version handling in docker and cd 2025-09-19 15:38:32 +03:00
Stavros
d70cbea546 chore: handle trusted proxies config error 2025-09-19 14:53:10 +03:00
Stavros
50105e4e9d feat: version info analytics (#363)
* feat: version info analytics

* refactor: don't create new client everytime
2025-09-19 14:44:22 +03:00
19 changed files with 184 additions and 92 deletions

View File

@@ -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=

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 == "" {

View File

@@ -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

View File

@@ -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>

View File

@@ -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} />

View File

@@ -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")
}
}
}

View File

@@ -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"

View File

@@ -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)
} }

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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
} }
} }
} }

View File

@@ -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

View File

@@ -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",

View File

@@ -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]
} }

View File

@@ -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))
} }