Compare commits

..

2 Commits

Author SHA1 Message Date
Stavros
e8a7144302 Merge branch 'main' into feat/global-ip-filters 2026-01-08 15:24:13 +02:00
Stavros
e553dae3c4 feat: add support for global ip filters 2026-01-07 20:08:37 +02:00
13 changed files with 73 additions and 71 deletions

View File

@@ -16,7 +16,7 @@
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"i18next": "^25.7.4",
"i18next": "^25.7.3",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-resources-to-backend": "^1.2.1",
"input-otp": "^1.4.2",
@@ -27,7 +27,7 @@
"react-hook-form": "^7.70.0",
"react-i18next": "^16.5.1",
"react-markdown": "^10.1.0",
"react-router": "^7.12.0",
"react-router": "^7.11.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
@@ -43,7 +43,7 @@
"eslint": "^9.39.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"globals": "^17.0.0",
"globals": "^16.5.0",
"prettier": "3.7.4",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
@@ -563,7 +563,7 @@
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@17.0.0", "", {}, "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw=="],
"globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
@@ -589,7 +589,7 @@
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
"i18next": ["i18next@25.7.4", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw=="],
"i18next": ["i18next@25.7.3", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA=="],
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="],
@@ -807,7 +807,7 @@
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
"react-router": ["react-router@7.12.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw=="],
"react-router": ["react-router@7.11.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ=="],
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],

View File

@@ -22,7 +22,7 @@
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"i18next": "^25.7.4",
"i18next": "^25.7.3",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-resources-to-backend": "^1.2.1",
"input-otp": "^1.4.2",
@@ -33,7 +33,7 @@
"react-hook-form": "^7.70.0",
"react-i18next": "^16.5.1",
"react-markdown": "^10.1.0",
"react-router": "^7.12.0",
"react-router": "^7.11.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
@@ -49,7 +49,7 @@
"eslint": "^9.39.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"globals": "^17.0.0",
"globals": "^16.5.0",
"prettier": "3.7.4",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",

View File

@@ -2,6 +2,7 @@ package bootstrap
import (
"fmt"
"strings"
"github.com/steveiliop56/tinyauth/internal/controller"
"github.com/steveiliop56/tinyauth/internal/middleware"
@@ -14,7 +15,7 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
engine.Use(gin.Recovery())
if len(app.config.Server.TrustedProxies) > 0 {
err := engine.SetTrustedProxies(app.config.Server.TrustedProxies)
err := engine.SetTrustedProxies(strings.Split(app.config.Server.TrustedProxies, ","))
if err != nil {
return nil, fmt.Errorf("failed to set trusted proxies: %w", err)

View File

@@ -33,15 +33,15 @@ type Config struct {
}
type ServerConfig struct {
Port int `description:"The port on which the server listens." yaml:"port"`
Address string `description:"The address on which the server listens." yaml:"address"`
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"`
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
Port int `description:"The port on which the server listens." yaml:"port"`
Address string `description:"The address on which the server listens." yaml:"address"`
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"`
TrustedProxies string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
}
type AuthConfig struct {
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
Users string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"`
@@ -56,7 +56,7 @@ type IPConfig struct {
}
type OAuthConfig struct {
Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"`
Whitelist string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"`
AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"`
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"`
}

View File

@@ -57,7 +57,7 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test
},
},
OauthWhitelist: []string{},
OauthWhitelist: "",
SessionExpiry: 3600,
SessionMaxLifetime: 0,
SecureCookie: false,

View File

@@ -60,7 +60,7 @@ func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Eng
TotpSecret: totpSecret,
},
},
OauthWhitelist: []string{},
OauthWhitelist: "",
SessionExpiry: 3600,
SessionMaxLifetime: 0,
SecureCookie: false,

View File

@@ -1,7 +1,7 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: queries.sql
// source: query.sql
package repository

View File

@@ -27,7 +27,7 @@ type LoginAttempt struct {
type AuthServiceConfig struct {
Users []config.User
OauthWhitelist []string
OauthWhitelist string
SessionExpiry int
SessionMaxLifetime int
SecureCookie bool
@@ -187,7 +187,7 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
}
func (auth *AuthService) IsEmailWhitelisted(email string) bool {
return utils.CheckFilter(strings.Join(auth.config.OauthWhitelist, ","), email)
return utils.CheckFilter(auth.config.OauthWhitelist, email)
}
func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.SessionCookie) error {

View File

@@ -7,14 +7,22 @@ import (
"github.com/steveiliop56/tinyauth/internal/config"
)
func ParseUsers(usersStr []string) ([]config.User, error) {
var users []config.User
func ParseUsers(users string) ([]config.User, error) {
var usersParsed []config.User
if len(usersStr) == 0 {
users = strings.TrimSpace(users)
if users == "" {
return []config.User{}, nil
}
for _, user := range usersStr {
userList := strings.Split(users, ",")
if len(userList) == 0 {
return []config.User{}, errors.New("invalid user format")
}
for _, user := range userList {
if strings.TrimSpace(user) == "" {
continue
}
@@ -22,71 +30,64 @@ func ParseUsers(usersStr []string) ([]config.User, error) {
if err != nil {
return []config.User{}, err
}
users = append(users, parsed)
usersParsed = append(usersParsed, parsed)
}
return users, nil
return usersParsed, nil
}
func GetUsers(usersCfg []string, usersPath string) ([]config.User, error) {
var usersStr []string
func GetUsers(conf string, file string) ([]config.User, error) {
var users string
if len(usersCfg) == 0 && usersPath == "" {
if conf == "" && file == "" {
return []config.User{}, nil
}
if len(usersCfg) > 0 {
usersStr = append(usersStr, usersCfg...)
if conf != "" {
users += conf
}
if usersPath != "" {
contents, err := ReadFile(usersPath)
if file != "" {
contents, err := ReadFile(file)
if err != nil {
return []config.User{}, err
}
lines := strings.SplitSeq(contents, "\n")
for line := range lines {
lineTrimmed := strings.TrimSpace(line)
if lineTrimmed == "" {
continue
}
usersStr = append(usersStr, lineTrimmed)
if users != "" {
users += ","
}
users += ParseFileToLine(contents)
}
return ParseUsers(usersStr)
return ParseUsers(users)
}
func ParseUser(userStr string) (config.User, error) {
if strings.Contains(userStr, "$$") {
userStr = strings.ReplaceAll(userStr, "$$", "$")
func ParseUser(user string) (config.User, error) {
if strings.Contains(user, "$$") {
user = strings.ReplaceAll(user, "$$", "$")
}
parts := strings.SplitN(userStr, ":", 4)
userSplit := strings.Split(user, ":")
if len(parts) < 2 || len(parts) > 3 {
if len(userSplit) < 2 || len(userSplit) > 3 {
return config.User{}, errors.New("invalid user format")
}
for i, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed == "" {
for _, userPart := range userSplit {
if strings.TrimSpace(userPart) == "" {
return config.User{}, errors.New("invalid user format")
}
parts[i] = trimmed
}
user := config.User{
Username: parts[0],
Password: parts[1],
if len(userSplit) == 2 {
return config.User{
Username: strings.TrimSpace(userSplit[0]),
Password: strings.TrimSpace(userSplit[1]),
}, nil
}
if len(parts) == 3 {
user.TotpSecret = parts[2]
}
return user, nil
return config.User{
Username: strings.TrimSpace(userSplit[0]),
Password: strings.TrimSpace(userSplit[1]),
TotpSecret: strings.TrimSpace(userSplit[2]),
}, nil
}

View File

@@ -22,7 +22,7 @@ func TestGetUsers(t *testing.T) {
defer os.Remove("/tmp/tinyauth_users_test.txt")
// Test file
users, err := utils.GetUsers([]string{}, "/tmp/tinyauth_users_test.txt")
users, err := utils.GetUsers("", "/tmp/tinyauth_users_test.txt")
assert.NilError(t, err)
@@ -34,7 +34,7 @@ func TestGetUsers(t *testing.T) {
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
// Test config
users, err = utils.GetUsers([]string{"user3:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "user4:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G"}, "")
users, err = utils.GetUsers("user3:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G,user4:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "")
assert.NilError(t, err)
@@ -46,7 +46,7 @@ func TestGetUsers(t *testing.T) {
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
// Test both
users, err = utils.GetUsers([]string{"user5:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G"}, "/tmp/tinyauth_users_test.txt")
users, err = utils.GetUsers("user5:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "/tmp/tinyauth_users_test.txt")
assert.NilError(t, err)
@@ -60,14 +60,14 @@ func TestGetUsers(t *testing.T) {
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[2].Password)
// Test empty
users, err = utils.GetUsers([]string{}, "")
users, err = utils.GetUsers("", "")
assert.NilError(t, err)
assert.Equal(t, 0, len(users))
// Test non-existent file
users, err = utils.GetUsers([]string{}, "/tmp/non_existent_file.txt")
users, err = utils.GetUsers("", "/tmp/non_existent_file.txt")
assert.ErrorContains(t, err, "no such file or directory")
@@ -76,7 +76,7 @@ func TestGetUsers(t *testing.T) {
func TestParseUsers(t *testing.T) {
// Valid users
users, err := utils.ParseUsers([]string{"user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF"}) // user2 has TOTP
users, err := utils.ParseUsers("user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G,user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF") // user2 has TOTP
assert.NilError(t, err)
@@ -90,7 +90,7 @@ func TestParseUsers(t *testing.T) {
assert.Equal(t, "ABCDEF", users[1].TotpSecret)
// Valid weirdly spaced users
users, err = utils.ParseUsers([]string{" user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G ", " user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF "}) // Spacing is on purpose
users, err = utils.ParseUsers(" user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G , user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF ") // Spacing is on purpose
assert.NilError(t, err)
assert.Equal(t, 2, len(users))

View File

@@ -1,8 +1,8 @@
version: "2"
sql:
- engine: "sqlite"
queries: "sql/queries.sql"
schema: "sql/schema.sql"
queries: "query.sql"
schema: "schema.sql"
gen:
go:
package: "repository"