Compare commits

...

22 Commits

Author SHA1 Message Date
Stavros
f61b6dbad4 refactor: log errors 2025-01-24 18:24:20 +02:00
Stavros
35854f5ce4 chore: bump version 2025-01-24 18:17:05 +02:00
Stavros
c59aaa5600 feat: add option to disable continue screen 2025-01-24 18:16:23 +02:00
Stavros
085b1492cc fix: ignore new lines in password file 2025-01-24 17:51:32 +02:00
Stavros
a19f3589f8 Merge pull request #5 from steveiliop56/feat/oauth
feat/oauth
2025-01-24 17:45:55 +02:00
Stavros
e88ec22ce3 fix: fix spacing in logout screen 2025-01-24 17:43:12 +02:00
Stavros
90f4c3c980 feat: generic oauth 2025-01-24 17:13:51 +02:00
Stavros
f487e25ac5 refactor: remove microsoft icon 2025-01-24 16:55:03 +02:00
Stavros
d4eca52b12 feat: google oauth 2025-01-24 16:29:21 +02:00
Stavros
433e71bd50 feat: persist sessions and auto redirect to app 2025-01-24 15:29:46 +02:00
Stavros
80d25551e0 wip 2025-01-23 19:16:35 +02:00
Stavros
143b13af2c refactor: remove short flags 2025-01-22 21:50:01 +02:00
Stavros
4457d6f525 feat: add cookie secure option in the cli 2025-01-22 21:37:59 +02:00
Stavros
b901744e03 fix: use password hash instead of password when verifying 2025-01-22 20:18:03 +02:00
Stavros
61a7400cf1 refactor: change cmd to entrypoint 2025-01-22 17:26:46 +02:00
Stavros
40ab77cdd5 chore: bump version 2025-01-22 16:05:42 +02:00
Stavros
403787e56c feat: verify user cmd 2025-01-22 16:05:01 +02:00
Stavros
d3e52c925d feat: create user command 2025-01-21 23:18:02 +02:00
Stavros
a4c717ba34 chore: remove screenshots 2025-01-21 18:42:02 +02:00
Stavros
5e73d06fcc refactor: use dependency injection 2025-01-21 18:41:06 +02:00
Stavros
2988b5f22f feat: add option to make cookie secure 2025-01-21 18:13:18 +02:00
Stavros
a28e55ae4c fix: split domain correctly 2025-01-21 18:09:22 +02:00
31 changed files with 1281 additions and 146 deletions

View File

@@ -38,7 +38,7 @@ COPY --from=site-builder /site/dist ./internal/assets/dist
RUN 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"]

View File

@@ -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,122 @@ 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 config
oauthConfig := types.OAuthConfig{
GithubClientId: config.GithubClientId,
GithubClientSecret: config.GithubClientSecret,
GoogleClientId: config.GoogleClientId,
GoogleClientSecret: config.GoogleClientSecret,
GenericClientId: config.GenericClientId,
GenericClientSecret: config.GenericClientSecret,
GenericScopes: config.GenericScopes,
GenericAuthURL: config.GenericAuthURL,
GenericTokenURL: config.GenericTokenURL,
GenericUserInfoURL: config.GenericUserInfoURL,
AppURL: config.AppURL,
}
// Create auth service
auth := auth.NewAuth(users)
// 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-info-url", "", "Generic OAuth user info URL.")
rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
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-info-url", "GENERIC_USER_INFO_URL")
viper.BindEnv("disable-continue", "DISABLE_CONTINUE")
viper.BindPFlags(rootCmd.Flags())
}

79
cmd/user/create/create.go Normal file
View 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
View 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
View 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
View File

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

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

View File

@@ -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,65 @@ 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)
log.Info().Str("domain", domain).Msg("Using domain for cookies")
if domainErr != nil {
log.Fatal().Err(domainErr).Msg("Failed to get domain")
os.Exit(1)
}
var isSecure bool
if api.Config.CookieSecure {
isSecure = true
} else {
isSecure = false
}
api.Domain = fmt.Sprintf(".%s", domain)
store.Options(sessions.Options{
Domain: fmt.Sprintf(".%s", domain),
Path: "/",
Domain: api.Domain,
Path: "/",
HttpOnly: true,
Secure: isSecure,
})
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 +92,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 +124,236 @@ 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 bindErr != nil {
log.Error().Err(bindErr).Msg("Failed to bind URI")
c.JSON(400, gin.H{
"status": 400,
"message": "Bad Request",
})
return
}
code := c.Query("code")
if code == "" {
c.JSON(400, gin.H{
"status": 400,
"message": "Bad Request",
})
return
}
provider := api.Providers.GetProvider(providerName.Provider)
if provider == nil {
c.JSON(404, gin.H{
"status": 404,
"message": "Not Found",
})
return
}
token, tokenErr := provider.ExchangeToken(code)
if tokenErr != nil {
log.Error().Err(tokenErr).Msg("Failed to exchange token")
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
})
return
}
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)
queries, queryErr := query.Values(types.LoginQuery{
RedirectURI: redirectURI,
})
if queryErr != nil {
log.Error().Err(queryErr).Msg("Failed to build query")
c.JSON(501, gin.H{
"status": 501,
"message": "Internal Server Error",
})
return
}
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/continue?%s", api.Config.AppURL, queries.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 +366,16 @@ 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")
}
}
}
}

View File

@@ -1 +1 @@
v0.2.0
v1.0.0

View File

@@ -6,16 +6,26 @@ 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) *Auth {
return &Auth{
Users: userList,
}
}
type Auth struct {
Users types.Users
}
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
}

View File

@@ -1,44 +1,114 @@
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
}
return types.UserContext{
Username: username,
Email: email,
IsLoggedIn: true,
}
}
OAuth: true,
Provider: sessionType,
}, nil
}

43
internal/oauth/oauth.go Normal file
View 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)
}

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

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

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

View 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: []string{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.GenericUserInfoURL)
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
}

View File

@@ -1,33 +1,80 @@
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"`
GenericUserInfoURL string `mapstructure:"generic-user-info-url"`
DisableContinue bool `mapstructure:"disable-continue"`
}
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
DisableContinue bool
}
type OAuthConfig struct {
GithubClientId string
GithubClientSecret string
GoogleClientId string
GoogleClientSecret string
GenericClientId string
GenericClientSecret string
GenericScopes string
GenericAuthURL string
GenericTokenURL string
GenericUserInfoURL string
AppURL string
}
type OAuthRequest struct {
Provider string `uri:"provider" binding:"required"`
}
type OAuthProviders struct {
Github *oauth.OAuth
Google *oauth.OAuth
Microsoft *oauth.OAuth
}

View File

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

15
main.go
View File

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

View File

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

View File

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

View File

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

View File

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

@@ -0,0 +1 @@
export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);