mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-10-28 12:45:47 +00:00
Compare commits
31 Commits
v0.1.0
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f61b6dbad4 | ||
|
|
35854f5ce4 | ||
|
|
c59aaa5600 | ||
|
|
085b1492cc | ||
|
|
a19f3589f8 | ||
|
|
e88ec22ce3 | ||
|
|
90f4c3c980 | ||
|
|
f487e25ac5 | ||
|
|
d4eca52b12 | ||
|
|
433e71bd50 | ||
|
|
80d25551e0 | ||
|
|
143b13af2c | ||
|
|
4457d6f525 | ||
|
|
b901744e03 | ||
|
|
61a7400cf1 | ||
|
|
40ab77cdd5 | ||
|
|
403787e56c | ||
|
|
d3e52c925d | ||
|
|
a4c717ba34 | ||
|
|
5e73d06fcc | ||
|
|
2988b5f22f | ||
|
|
a28e55ae4c | ||
|
|
6596e4dea6 | ||
|
|
a2e6231cc4 | ||
|
|
644b343a1b | ||
|
|
7817add9b4 | ||
|
|
fcaa3779d5 | ||
|
|
e2f97d1fbe | ||
|
|
4f4645f32b | ||
|
|
d0c1aae1e7 | ||
|
|
b8a134ed12 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -2,4 +2,10 @@
|
||||
internal/assets/dist
|
||||
|
||||
# binaries
|
||||
tinyauth
|
||||
tinyauth
|
||||
|
||||
# test docker compose
|
||||
docker-compose.test.yml
|
||||
|
||||
# users file
|
||||
users.txt
|
||||
@@ -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"]
|
||||
34
README.md
34
README.md
@@ -1,38 +1,16 @@
|
||||
# Tinyauth - The easiest way to secure your traefik apps with a login screen
|
||||
# Tinyauth - The simplest way to protect your apps with a login screen
|
||||
|
||||
Tinyauth is an extremely simple traefik forward auth proxy that adds a login screen to all of your apps that are using the traefik reverse proxy. Tinyauth is configurable through environment variables and it is only 20MB in size.
|
||||
|
||||
## Screenshots
|
||||
|
||||
Talk is cheap, here are some screenshots:
|
||||
|
||||
| | |
|
||||
| --------------------------------------- | ----------------------------------------- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
Tinyauth is an extremely simple traefik middleware that adds a login screen to all of your apps that are using the traefik reverse proxy. Tinyauth is configurable through environment variables and it is only 20MB in size.
|
||||
|
||||
## Getting started
|
||||
|
||||
Tinyauth is extremely easy to run since it's shipped as a docker container. I chose to bundle it with busybox so as you can easily debug the API (e.g. using curl) and have some simple linux tools. If you want to get started with an example just check the example docker compose file [here](./docker-compose.example.yml)
|
||||
|
||||
## Environment variables
|
||||
|
||||
Tinyauth accepts the following environment variables:
|
||||
|
||||
| Name | Description | Default | Required |
|
||||
| ---------- | ------------------------------------------------------- | ------- | -------- |
|
||||
| `PORT` | The port the API listens on. | 3000 | no |
|
||||
| `ADDRESS` | The address the API binds on. | 0.0.0.0 | no |
|
||||
| `SECRET` | A 32 character long string used for the sessions. | - | yes |
|
||||
| `ROOT_URL` | The base URL of your domain. (e.g. https://example.com) | - | yes |
|
||||
| `APP_URL` | The Tinyauth URL. (e.g. https://tinyauth.example.com) | - | yes |
|
||||
| `USERS` | Comma seperated list of `user:bcrypt-password-hash`. | - | yes |
|
||||
Tinyauth is extremely easy to run since it's shipped as a docker container. The guide on how to get started is available on the website [here](https://tinyauth.doesmycode.work/).
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why?
|
||||
|
||||
Why make this project? Well we all know that more powerful alternatives like authentik and authelia exist but when I tried to use them I felt overwhelmed with all the configration options and environment variables I had to configure in order for them to work, so, I decided to make a small alternative in Go to both test my skills and cover my simple login screen needs.
|
||||
Why make this project? Well, we all know that more powerful alternatives like authentik and authelia exist, but when I tried to use them, I felt overwhelmed with all the configration options and environment variables I had to configure in order for them to work. So, I decided to make a small alternative in Go to both test my skills and cover my simple login screen needs.
|
||||
|
||||
### Is this secure?
|
||||
|
||||
@@ -40,7 +18,7 @@ Probably, the sessions are managed with the gin sessions package so it should be
|
||||
|
||||
### Do I need to login every time?
|
||||
|
||||
No, when you login tinyauth sets a `tinyauth` cookie in your browser that applies to all of the subdomains of the root URL you set.
|
||||
No, when you login, tinyauth sets a `tinyauth` cookie in your browser that applies to all of the subdomains of your domain.
|
||||
|
||||
## License
|
||||
|
||||
@@ -48,7 +26,7 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma
|
||||
|
||||
## Contributing
|
||||
|
||||
Any contributions to the codebase are welcome! I am not a cybersecurity person so my code may have some vulnerability, if you find something that could be used to exploit and bypass tinyauth please tell me as soon as possible so I can fix it.
|
||||
Any contributions to the codebase are welcome! I am not a cybersecurity person so my code may have a security issue, if you find something that could be used to exploit and bypass tinyauth please let me know as soon as possible so I can fix it.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
||||
121
cmd/root.go
121
cmd/root.go
@@ -1,15 +1,15 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"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"
|
||||
@@ -18,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
|
||||
@@ -36,44 +32,127 @@ var rootCmd = &cobra.Command{
|
||||
validateErr := validator.Struct(config)
|
||||
HandleError(validateErr, "Invalid config")
|
||||
|
||||
// Create users list
|
||||
log.Info().Msg("Creating users list")
|
||||
userList, createErr := utils.CreateUsersList(config.Users)
|
||||
HandleError(createErr, "Failed to create users list")
|
||||
// Parse users
|
||||
log.Info().Msg("Parsing users")
|
||||
|
||||
// Start server
|
||||
log.Info().Msg("Starting server")
|
||||
api.Run(config, userList)
|
||||
if config.UsersFile == "" && config.Users == "" {
|
||||
log.Fatal().Msg("No users provided")
|
||||
}
|
||||
|
||||
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 := utils.ParseFileToLine(usersFromFile)
|
||||
if usersString != "" {
|
||||
usersString = usersString + "," + usersFromFileParsed
|
||||
} else {
|
||||
usersString = usersFromFileParsed
|
||||
}
|
||||
}
|
||||
|
||||
users, parseErr := utils.ParseUsers(usersString)
|
||||
HandleError(parseErr, "Failed to parse users")
|
||||
|
||||
// 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("root-url", "", "Root URL of traefik.")
|
||||
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("root-url", "ROOT_URL")
|
||||
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
79
cmd/user/create/create.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package create
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var interactive bool
|
||||
var username string
|
||||
var password string
|
||||
var docker bool
|
||||
|
||||
var CreateCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a user",
|
||||
Long: `Create a user either interactively or by passing flags.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if interactive {
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("Username").Value(&username).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("username cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewInput().Title("Password").Value(&password).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("password cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewSelect[bool]().Title("Format the output for docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&docker),
|
||||
),
|
||||
)
|
||||
|
||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||
|
||||
formErr := form.WithTheme(baseTheme).Run()
|
||||
|
||||
if formErr != nil {
|
||||
log.Fatal().Err(formErr).Msg("Form failed")
|
||||
}
|
||||
}
|
||||
|
||||
if username == "" || password == "" {
|
||||
log.Error().Msg("Username and password cannot be empty")
|
||||
}
|
||||
|
||||
log.Info().Str("username", username).Str("password", password).Bool("docker", docker).Msg("Creating user")
|
||||
|
||||
passwordByte, passwordErr := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
|
||||
if passwordErr != nil {
|
||||
log.Fatal().Err(passwordErr).Msg("Failed to hash password")
|
||||
}
|
||||
|
||||
passwordString := string(passwordByte)
|
||||
|
||||
if docker {
|
||||
passwordString = strings.ReplaceAll(passwordString, "$", "$$")
|
||||
}
|
||||
|
||||
log.Info().Str("user", fmt.Sprintf("%s:%s", username, passwordString)).Msg("User created")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
CreateCmd.Flags().BoolVar(&interactive, "interactive", false, "Create a user interactively")
|
||||
CreateCmd.Flags().BoolVar(&docker, "docker", false, "Format output for docker")
|
||||
CreateCmd.Flags().StringVar(&username, "username", "", "Username")
|
||||
CreateCmd.Flags().StringVar(&password, "password", "", "Password")
|
||||
}
|
||||
19
cmd/user/user.go
Normal file
19
cmd/user/user.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"tinyauth/cmd/user/create"
|
||||
"tinyauth/cmd/user/verify"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func UserCmd() *cobra.Command {
|
||||
userCmd := &cobra.Command{
|
||||
Use: "user",
|
||||
Short: "User utilities",
|
||||
Long: `Utilities for creating and verifying tinyauth compatible users.`,
|
||||
}
|
||||
userCmd.AddCommand(create.CreateCmd)
|
||||
userCmd.AddCommand(verify.VerifyCmd)
|
||||
return userCmd
|
||||
}
|
||||
90
cmd/user/verify/verify.go
Normal file
90
cmd/user/verify/verify.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package verify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var interactive bool
|
||||
var username string
|
||||
var password string
|
||||
var docker bool
|
||||
var user string
|
||||
|
||||
var VerifyCmd = &cobra.Command{
|
||||
Use: "verify",
|
||||
Short: "Verify a user is set up correctly",
|
||||
Long: `Verify a user is set up correctly meaning that it has a correct password.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if interactive {
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("User (user:hash)").Value(&user).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("user cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewInput().Title("Username").Value(&username).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("username cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewInput().Title("Password").Value(&password).Validate((func(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("password cannot be empty")
|
||||
}
|
||||
return nil
|
||||
})),
|
||||
huh.NewSelect[bool]().Title("Is the user formatted for docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&docker),
|
||||
),
|
||||
)
|
||||
|
||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||
|
||||
formErr := form.WithTheme(baseTheme).Run()
|
||||
|
||||
if formErr != nil {
|
||||
log.Fatal().Err(formErr).Msg("Form failed")
|
||||
}
|
||||
}
|
||||
|
||||
if username == "" || password == "" || user == "" {
|
||||
log.Fatal().Msg("Username, password and user cannot be empty")
|
||||
}
|
||||
|
||||
log.Info().Str("user", user).Str("username", username).Str("password", password).Bool("docker", docker).Msg("Verifying user")
|
||||
|
||||
userSplit := strings.Split(user, ":")
|
||||
|
||||
if userSplit[1] == "" {
|
||||
log.Fatal().Msg("User is not formatted correctly")
|
||||
}
|
||||
|
||||
if docker {
|
||||
userSplit[1] = strings.ReplaceAll(userSplit[1], "$$", "$")
|
||||
}
|
||||
|
||||
verifyErr := bcrypt.CompareHashAndPassword([]byte(userSplit[1]), []byte(password))
|
||||
|
||||
if verifyErr != nil || username != userSplit[0] {
|
||||
log.Fatal().Msg("Username or password incorrect")
|
||||
} else {
|
||||
log.Info().Msg("Verification successful")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
VerifyCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively")
|
||||
VerifyCmd.Flags().BoolVar(&docker, "docker", false, "Is the user formatted for docker?")
|
||||
VerifyCmd.Flags().StringVar(&username, "username", "", "Username")
|
||||
VerifyCmd.Flags().StringVar(&password, "password", "", "Password")
|
||||
VerifyCmd.Flags().StringVar(&user, "user", "", "Hash (user:hash combination)")
|
||||
}
|
||||
34
docker-compose.dev.yml
Normal file
34
docker-compose.dev.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
services:
|
||||
traefik:
|
||||
container_name: traefik
|
||||
image: traefik:v3.3
|
||||
command: --api.insecure=true --providers.docker
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
labels:
|
||||
traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth:3000/api/auth
|
||||
|
||||
nginx:
|
||||
container_name: nginx
|
||||
image: nginx:latest
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.nginx.rule: Host(`nginx.dev.local`)
|
||||
traefik.http.services.nginx.loadbalancer.server.port: 80
|
||||
traefik.http.routers.nginx.middlewares: tinyauth
|
||||
|
||||
tinyauth:
|
||||
container_name: tinyauth
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
- SECRET=some-random-32-chars-string
|
||||
- APP_URL=http://tinyauth.dev.local
|
||||
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.tinyauth.rule: Host(`tinyauth.dev.local`)
|
||||
traefik.http.services.tinyauth.loadbalancer.server.port: 3000
|
||||
@@ -15,7 +15,7 @@ services:
|
||||
image: nginx:latest
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.nginx.rule: Host(`nginx.dev.local`)
|
||||
traefik.http.routers.nginx.rule: Host(`nginx.example.com`)
|
||||
traefik.http.services.nginx.loadbalancer.server.port: 80
|
||||
traefik.http.routers.nginx.middlewares: tinyauth
|
||||
|
||||
@@ -24,10 +24,9 @@ services:
|
||||
image: ghcr.io/steveiliop56/tinyauth:latest
|
||||
environment:
|
||||
- SECRET=some-random-32-chars-string
|
||||
- ROOT_URL=https://example.com
|
||||
- APP_URL=https://tinyauth.example.com
|
||||
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u
|
||||
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.tinyauth.rule: Host(`tinyauth.dev.local`)
|
||||
traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`)
|
||||
traefik.http.services.tinyauth.loadbalancer.server.port: 3000
|
||||
|
||||
34
go.mod
34
go.mod
@@ -3,23 +3,38 @@ module tinyauth
|
||||
go 1.23.2
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/sessions v1.0.2
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/go-playground/validator/v10 v10.24.0
|
||||
github.com/google/go-querystring v1.1.0
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/viper v1.19.0
|
||||
golang.org/x/crypto v0.32.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.12.7 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.3 // indirect
|
||||
github.com/catppuccin/go v0.2.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.20.0 // indirect
|
||||
github.com/charmbracelet/bubbletea v1.1.0 // indirect
|
||||
github.com/charmbracelet/huh v0.6.0 // indirect
|
||||
github.com/charmbracelet/lipgloss v0.13.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.2.3 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sessions v1.0.2 // indirect
|
||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.24.0 // indirect
|
||||
github.com/goccy/go-json v0.10.4 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.2.2 // indirect
|
||||
@@ -28,30 +43,37 @@ require (
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/rs/zerolog v1.33.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.19.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.13.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/oauth2 v0.25.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/protobuf v1.36.3 // indirect
|
||||
|
||||
70
go.sum
70
go.sum
@@ -1,16 +1,43 @@
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
|
||||
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
|
||||
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
||||
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
|
||||
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
|
||||
github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c=
|
||||
github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=
|
||||
github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8=
|
||||
github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU=
|
||||
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
|
||||
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
|
||||
github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY=
|
||||
github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
|
||||
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
@@ -33,11 +60,13 @@ github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
@@ -54,8 +83,14 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@@ -65,6 +100,12 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -72,11 +113,23 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
|
||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
@@ -127,6 +180,11 @@ golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjs
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -134,12 +192,12 @@ golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -9,7 +9,10 @@ import (
|
||||
"time"
|
||||
"tinyauth/internal/assets"
|
||||
"tinyauth/internal/auth"
|
||||
"tinyauth/internal/hooks"
|
||||
"tinyauth/internal/providers"
|
||||
"tinyauth/internal/types"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
@@ -18,27 +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 := strings.Split(config.RootURL, "://")[1]
|
||||
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") {
|
||||
@@ -51,21 +92,28 @@ func Run(config types.Config, users types.UserList) {
|
||||
}
|
||||
})
|
||||
|
||||
router.GET("/api/auth", func (c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
value := session.Get("tinyauth")
|
||||
api.Router = router
|
||||
}
|
||||
|
||||
if value != nil {
|
||||
usernameString, ok := value.(string)
|
||||
if ok {
|
||||
if auth.FindUser(users, usernameString) != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"message": "Authorized",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
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,
|
||||
"message": "Authenticated",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uri := c.Request.Header.Get("X-Forwarded-Uri")
|
||||
@@ -76,103 +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) {
|
||||
session := sessions.Default(c)
|
||||
value := session.Get("tinyauth")
|
||||
api.Router.GET("/api/status", func(c *gin.Context) {
|
||||
userContext, userContextErr := api.Hooks.UseUserContext(c)
|
||||
|
||||
if value != nil {
|
||||
usernameString, ok := value.(string)
|
||||
if ok {
|
||||
if auth.FindUser(users, usernameString) != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"isLoggedIn": true,
|
||||
"username": usernameString,
|
||||
"version": assets.Version,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
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",
|
||||
"email": "",
|
||||
"isLoggedIn": false,
|
||||
"oauth": false,
|
||||
"provider": "",
|
||||
"configuredProviders": api.Providers.GetConfiguredProviders(),
|
||||
"disableContinue": api.Config.DisableContinue,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"isLoggedIn": false,
|
||||
"username": "",
|
||||
"version": assets.Version,
|
||||
"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 {
|
||||
@@ -185,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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
v0.1.0
|
||||
v1.0.0
|
||||
@@ -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
|
||||
}
|
||||
114
internal/hooks/hooks.go
Normal file
114
internal/hooks/hooks.go
Normal file
@@ -0,0 +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 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)
|
||||
sessionCookie := session.Get("tinyauth_sid")
|
||||
|
||||
if sessionCookie == nil {
|
||||
return types.UserContext{
|
||||
Email: "",
|
||||
IsLoggedIn: false,
|
||||
OAuth: false,
|
||||
Provider: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
data, dataOk := sessionCookie.(string)
|
||||
|
||||
if !dataOk {
|
||||
return types.UserContext{
|
||||
Email: "",
|
||||
IsLoggedIn: false,
|
||||
OAuth: false,
|
||||
Provider: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
split := strings.Split(data, ":")
|
||||
|
||||
if len(split) != 2 {
|
||||
return types.UserContext{
|
||||
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{
|
||||
Email: email,
|
||||
IsLoggedIn: true,
|
||||
OAuth: true,
|
||||
Provider: sessionType,
|
||||
}, nil
|
||||
}
|
||||
43
internal/oauth/oauth.go
Normal file
43
internal/oauth/oauth.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func NewOAuth(config oauth2.Config) *OAuth {
|
||||
return &OAuth{
|
||||
Config: config,
|
||||
}
|
||||
}
|
||||
|
||||
type OAuth struct {
|
||||
Config oauth2.Config
|
||||
Context context.Context
|
||||
Token *oauth2.Token
|
||||
Verifier string
|
||||
}
|
||||
|
||||
func (oauth *OAuth) Init() {
|
||||
oauth.Context = context.Background()
|
||||
oauth.Verifier = oauth2.GenerateVerifier()
|
||||
}
|
||||
|
||||
func (oauth *OAuth) GetAuthURL() string {
|
||||
return oauth.Config.AuthCodeURL("state", oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(oauth.Verifier))
|
||||
}
|
||||
|
||||
func (oauth *OAuth) ExchangeToken(code string) (string, error) {
|
||||
token, err := oauth.Config.Exchange(oauth.Context, code, oauth2.VerifierOption(oauth.Verifier))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
oauth.Token = token
|
||||
return oauth.Token.AccessToken, nil
|
||||
}
|
||||
|
||||
func (oauth *OAuth) GetClient() *http.Client {
|
||||
return oauth.Config.Client(oauth.Context, oauth.Token)
|
||||
}
|
||||
35
internal/providers/generic.go
Normal file
35
internal/providers/generic.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type GenericUserInfoResponse struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func GetGenericEmail(client *http.Client, url string) (string, error) {
|
||||
res, resErr := client.Get(url)
|
||||
|
||||
if resErr != nil {
|
||||
return "", resErr
|
||||
}
|
||||
|
||||
body, bodyErr := io.ReadAll(res.Body)
|
||||
|
||||
if bodyErr != nil {
|
||||
return "", bodyErr
|
||||
}
|
||||
|
||||
var user GenericUserInfoResponse
|
||||
|
||||
jsonErr := json.Unmarshal(body, &user)
|
||||
|
||||
if jsonErr != nil {
|
||||
return "", jsonErr
|
||||
}
|
||||
|
||||
return user.Email, nil
|
||||
}
|
||||
47
internal/providers/github.go
Normal file
47
internal/providers/github.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type GithubUserInfoResponse []struct {
|
||||
Email string `json:"email"`
|
||||
Primary bool `json:"primary"`
|
||||
}
|
||||
|
||||
func GithubScopes() []string {
|
||||
return []string{"user:email"}
|
||||
}
|
||||
|
||||
func GetGithubEmail(client *http.Client) (string, error) {
|
||||
res, resErr := client.Get("https://api.github.com/user/emails")
|
||||
|
||||
if resErr != nil {
|
||||
return "", resErr
|
||||
}
|
||||
|
||||
body, bodyErr := io.ReadAll(res.Body)
|
||||
|
||||
if bodyErr != nil {
|
||||
return "", bodyErr
|
||||
}
|
||||
|
||||
var emails GithubUserInfoResponse
|
||||
|
||||
jsonErr := json.Unmarshal(body, &emails)
|
||||
|
||||
if jsonErr != nil {
|
||||
return "", jsonErr
|
||||
}
|
||||
|
||||
for _, email := range emails {
|
||||
if email.Primary {
|
||||
return email.Email, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("no primary email found")
|
||||
}
|
||||
39
internal/providers/google.go
Normal file
39
internal/providers/google.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type GoogleUserInfoResponse struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func GoogleScopes() []string {
|
||||
return []string{"https://www.googleapis.com/auth/userinfo.email"}
|
||||
}
|
||||
|
||||
func GetGoogleEmail(client *http.Client) (string, error) {
|
||||
res, resErr := client.Get("https://www.googleapis.com/userinfo/v2/me")
|
||||
|
||||
if resErr != nil {
|
||||
return "", resErr
|
||||
}
|
||||
|
||||
body, bodyErr := io.ReadAll(res.Body)
|
||||
|
||||
if bodyErr != nil {
|
||||
return "", bodyErr
|
||||
}
|
||||
|
||||
var user GoogleUserInfoResponse
|
||||
|
||||
jsonErr := json.Unmarshal(body, &user)
|
||||
|
||||
if jsonErr != nil {
|
||||
return "", jsonErr
|
||||
}
|
||||
|
||||
return user.Email, nil
|
||||
}
|
||||
127
internal/providers/providers.go
Normal file
127
internal/providers/providers.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"tinyauth/internal/oauth"
|
||||
"tinyauth/internal/types"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/endpoints"
|
||||
)
|
||||
|
||||
func NewProviders(config types.OAuthConfig) *Providers {
|
||||
return &Providers{
|
||||
Config: config,
|
||||
}
|
||||
}
|
||||
|
||||
type Providers struct {
|
||||
Config types.OAuthConfig
|
||||
Github *oauth.OAuth
|
||||
Google *oauth.OAuth
|
||||
Generic *oauth.OAuth
|
||||
}
|
||||
|
||||
func (providers *Providers) Init() {
|
||||
if providers.Config.GithubClientId != "" && providers.Config.GithubClientSecret != "" {
|
||||
log.Info().Msg("Initializing Github OAuth")
|
||||
providers.Github = oauth.NewOAuth(oauth2.Config{
|
||||
ClientID: providers.Config.GithubClientId,
|
||||
ClientSecret: providers.Config.GithubClientSecret,
|
||||
RedirectURL: fmt.Sprintf("%s/api/oauth/callback/github", providers.Config.AppURL),
|
||||
Scopes: GithubScopes(),
|
||||
Endpoint: endpoints.GitHub,
|
||||
})
|
||||
providers.Github.Init()
|
||||
}
|
||||
if providers.Config.GoogleClientId != "" && providers.Config.GoogleClientSecret != "" {
|
||||
log.Info().Msg("Initializing Google OAuth")
|
||||
providers.Google = oauth.NewOAuth(oauth2.Config{
|
||||
ClientID: providers.Config.GoogleClientId,
|
||||
ClientSecret: providers.Config.GoogleClientSecret,
|
||||
RedirectURL: fmt.Sprintf("%s/api/oauth/callback/google", providers.Config.AppURL),
|
||||
Scopes: GoogleScopes(),
|
||||
Endpoint: endpoints.Google,
|
||||
})
|
||||
providers.Google.Init()
|
||||
}
|
||||
if providers.Config.GenericClientId != "" && providers.Config.GenericClientSecret != "" {
|
||||
log.Info().Msg("Initializing Generic OAuth")
|
||||
providers.Generic = oauth.NewOAuth(oauth2.Config{
|
||||
ClientID: providers.Config.GenericClientId,
|
||||
ClientSecret: providers.Config.GenericClientSecret,
|
||||
RedirectURL: fmt.Sprintf("%s/api/oauth/callback/generic", providers.Config.AppURL),
|
||||
Scopes: []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
|
||||
}
|
||||
@@ -1,28 +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"`
|
||||
RootURL string `validate:"required,url" mapstructure:"root-url"`
|
||||
AppURL string `validate:"required,url" mapstructure:"app-url"`
|
||||
Users string `validate:"required" mapstructure:"users"`
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -2,28 +2,75 @@ package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"tinyauth/internal/types"
|
||||
)
|
||||
|
||||
func CreateUsersList(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("no users found")
|
||||
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) {
|
||||
urlParsed, parseErr := url.Parse(urlSrc)
|
||||
|
||||
if parseErr != nil {
|
||||
return "", parseErr
|
||||
}
|
||||
|
||||
urlSplitted := strings.Split(urlParsed.Host, ".")
|
||||
|
||||
urlFinal := strings.Join(urlSplitted[1:], ".")
|
||||
|
||||
return urlFinal, nil
|
||||
}
|
||||
|
||||
func GetUsersFromFile(usersFile string) (string, error) {
|
||||
_, statErr := os.Stat(usersFile)
|
||||
|
||||
if statErr != nil {
|
||||
return "", statErr
|
||||
}
|
||||
|
||||
data, readErr := os.ReadFile(usersFile)
|
||||
|
||||
if readErr != nil {
|
||||
return "", readErr
|
||||
}
|
||||
|
||||
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
15
main.go
@@ -1,7 +1,20 @@
|
||||
package main
|
||||
|
||||
import "tinyauth/cmd"
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
"tinyauth/cmd"
|
||||
"tinyauth/internal/assets"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Logger
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger()
|
||||
log.Info().Str("version", assets.Version).Msg("Starting tinyauth")
|
||||
|
||||
// Run cmd
|
||||
cmd.Execute()
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 64 KiB |
18
site/src/icons/github.tsx
Normal file
18
site/src/icons/github.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function GithubIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
30
site/src/icons/google.tsx
Normal file
30
site/src/icons/google.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function GoogleIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={48}
|
||||
height={48}
|
||||
viewBox="0 0 48 48"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#ffc107"
|
||||
d="M43.611 20.083H42V20H24v8h11.303c-1.649 4.657-6.08 8-11.303 8c-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4C12.955 4 4 12.955 4 24s8.955 20 20 20s20-8.955 20-20c0-1.341-.138-2.65-.389-3.917"
|
||||
></path>
|
||||
<path
|
||||
fill="#ff3d00"
|
||||
d="m6.306 14.691l6.571 4.819C14.655 15.108 18.961 12 24 12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4C16.318 4 9.656 8.337 6.306 14.691"
|
||||
></path>
|
||||
<path
|
||||
fill="#4caf50"
|
||||
d="M24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238A11.9 11.9 0 0 1 24 36c-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025C9.505 39.556 16.227 44 24 44"
|
||||
></path>
|
||||
<path
|
||||
fill="#1976d2"
|
||||
d="M43.611 20.083H42V20H24v8h11.303a12.04 12.04 0 0 1-4.087 5.571l.003-.002l6.19 5.238C36.971 39.205 44 34 44 24c0-1.341-.138-2.65-.389-3.917"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
24
site/src/icons/oauth.tsx
Normal file
24
site/src/icons/oauth.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function OAuthIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path d="M2 12a10 10 0 1 0 20 0a10 10 0 1 0-20 0"></path>
|
||||
<path d="M12.556 6c.65 0 1.235.373 1.508.947l2.839 7.848a1.646 1.646 0 0 1-1.01 2.108a1.673 1.673 0 0 1-2.068-.851L13.365 15h-2.73l-.398.905A1.67 1.67 0 0 1 8.26 16.95l-.153-.047a1.647 1.647 0 0 1-1.056-1.956l2.824-7.852a1.66 1.66 0 0 1 1.409-1.087z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
@@ -29,7 +33,7 @@ export const ContinuePage = () => {
|
||||
return (
|
||||
<Layout>
|
||||
<Paper shadow="md" p={30} mt={30} radius="md" withBorder>
|
||||
{redirectUri ? (
|
||||
{redirectUri !== "null" ? (
|
||||
<>
|
||||
<Text size="xl" fw={700}>
|
||||
Continue
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Button, Paper, Text } from "@mantine/core";
|
||||
import { Button, Code, Paper, Text } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
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,7 +44,8 @@ export const LogoutPage = () => {
|
||||
Logout
|
||||
</Text>
|
||||
<Text>
|
||||
You are currently logged in as {username}, click the button below to
|
||||
You are currently logged in as <Code>{email}</Code>
|
||||
{oauth && ` using ${capitalize(provider)}`}. Click the button below to
|
||||
log out.
|
||||
</Text>
|
||||
<Button
|
||||
|
||||
@@ -2,7 +2,11 @@ import { z } from "zod";
|
||||
|
||||
export const userContextSchema = z.object({
|
||||
isLoggedIn: z.boolean(),
|
||||
username: z.string(),
|
||||
email: z.string(),
|
||||
oauth: z.boolean(),
|
||||
provider: z.string(),
|
||||
configuredProviders: z.array(z.string()),
|
||||
disableContinue: z.boolean(),
|
||||
});
|
||||
|
||||
export type UserContextSchemaType = z.infer<typeof userContextSchema>;
|
||||
|
||||
1
site/src/utils/utils.ts
Normal file
1
site/src/utils/utils.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||
Reference in New Issue
Block a user