mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-06-03 18:10:16 +00:00
Compare commits
4 Commits
8b28e0c3e4
...
35cd3b9ce5
| Author | SHA1 | Date | |
|---|---|---|---|
| 35cd3b9ce5 | |||
| 90145dd774 | |||
| 9c0fe751cc | |||
| bc7c604a7d |
@@ -80,5 +80,17 @@
|
||||
"profileScopeDescription": "Allows the app to access your profile information.",
|
||||
"groupsScopeName": "Groups",
|
||||
"groupsScopeDescription": "Allows the app to access your group information.",
|
||||
"backToLoginButton": "Back to login"
|
||||
"backToLoginButton": "Back to login",
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "We detected that you are accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleSubmit": "Continue with Tailscale",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout."
|
||||
}
|
||||
|
||||
@@ -84,5 +84,13 @@
|
||||
"phoneScopeName": "Phone",
|
||||
"phoneScopeDescription": "Allows the app to access your phone number.",
|
||||
"addressScopeName": "Address",
|
||||
"addressScopeDescription": "Allows the app to access your address."
|
||||
"addressScopeDescription": "Allows the app to access your address.",
|
||||
"loginTailscaleTitle": "Continue with Tailscale",
|
||||
"loginTailscaleDescription": "We detected that you are accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?",
|
||||
"loginTailscaleDeviceName": "Device name:",
|
||||
"loginTailscaleSubmit": "Continue with Tailscale",
|
||||
"loginTailscaleOtherMethod": "Login with another method",
|
||||
"loginTailscaleSuccess": "Successfully authenticated with Tailscale.",
|
||||
"loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.",
|
||||
"logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device <code>{{deviceName}}</code>. Click the button below to logout."
|
||||
}
|
||||
|
||||
@@ -37,7 +37,11 @@ const iconMap: Record<string, React.ReactNode> = {
|
||||
|
||||
export const LoginPage = () => {
|
||||
const { auth, tailscale } = useUserContext();
|
||||
const { ui, oauth, auth: cauth } = useAppContext();
|
||||
const {
|
||||
ui,
|
||||
oauth,
|
||||
auth: { providers },
|
||||
} = useAppContext();
|
||||
const { search } = useLocation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -56,15 +60,15 @@ export const LoginPage = () => {
|
||||
const oidcParams = useOIDCParams(searchParams);
|
||||
|
||||
const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
|
||||
cauth.providers.find((provider) => provider.id === oauth.autoRedirect) !==
|
||||
providers.find((provider) => provider.id === oauth.autoRedirect) !==
|
||||
undefined && redirectUri !== undefined,
|
||||
);
|
||||
|
||||
const oauthProviders = cauth.providers.filter(
|
||||
const oauthProviders = providers.filter(
|
||||
(provider) => provider.id !== "local" && provider.id !== "ldap",
|
||||
);
|
||||
const userAuthConfigured =
|
||||
cauth.providers.find(
|
||||
providers.find(
|
||||
(provider) => provider.id === "local" || provider.id === "ldap",
|
||||
) !== undefined;
|
||||
|
||||
@@ -154,8 +158,8 @@ export const LoginPage = () => {
|
||||
mutationFn: () => axios.post("/api/user/tailscale"),
|
||||
mutationKey: ["tailscale"],
|
||||
onSuccess: () => {
|
||||
toast.success("Logged in", {
|
||||
description: t("Tailscale session confirmed"),
|
||||
toast.success(t("loginSuccessTitle"), {
|
||||
description: t("loginTailscaleSuccess"),
|
||||
});
|
||||
|
||||
redirectTimer.current = window.setTimeout(() => {
|
||||
@@ -169,8 +173,8 @@ export const LoginPage = () => {
|
||||
}, 500);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to login", {
|
||||
description: "Failed to authenticate with Tailscale.",
|
||||
toast.error(t("loginFailTitle"), {
|
||||
description: t("loginTailscaleFail"),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -262,17 +266,15 @@ export const LoginPage = () => {
|
||||
<CardHeader className="gap-3">
|
||||
<TailscaleIcon className="mx-auto h-8 w-8" />
|
||||
<CardTitle className="text-center text-xl">
|
||||
Tinyauth · Tailscale
|
||||
{t("loginTailscaleTitle")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
We detected that you are accessing Tinyauth from an authorized
|
||||
Tailscale device. Would you like to continue with your Tailscale
|
||||
credentials?
|
||||
{t("loginTailscaleDescription")}
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
Machine Name: <code>{tailscale.nodeName}</code>
|
||||
{t("loginTailscaleDeviceName")} <code>{tailscale.nodeName}</code>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col items-stretch gap-3">
|
||||
@@ -281,7 +283,7 @@ export const LoginPage = () => {
|
||||
onClick={() => tailscaleMutate()}
|
||||
loading={tailscaleIsPending}
|
||||
>
|
||||
Continue with Tailscale
|
||||
{t("loginTailscaleSubmit")}
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
@@ -289,7 +291,7 @@ export const LoginPage = () => {
|
||||
onClick={() => setUseTailscale(false)}
|
||||
disabled={tailscaleIsPending}
|
||||
>
|
||||
Use other login method
|
||||
{t("loginTailscaleOtherMethod")}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
@@ -300,7 +302,7 @@ export const LoginPage = () => {
|
||||
<Card>
|
||||
<CardHeader className="gap-1.5">
|
||||
<CardTitle className="text-center text-xl">{ui.title}</CardTitle>
|
||||
{cauth.providers.length > 0 && (
|
||||
{providers.length > 0 && (
|
||||
<CardDescription className="text-center">
|
||||
{oauthProviders.length !== 0
|
||||
? t("loginTitle")
|
||||
@@ -338,7 +340,7 @@ export const LoginPage = () => {
|
||||
})()}
|
||||
/>
|
||||
)}
|
||||
{cauth.providers.length == 0 && (
|
||||
{providers.length == 0 && (
|
||||
<pre className="break-normal! text-sm text-red-600">
|
||||
{t("failedToFetchProvidersTitle")}
|
||||
</pre>
|
||||
|
||||
@@ -75,9 +75,17 @@ export const LogoutPage = () => {
|
||||
if (auth.providerId === "tailscale") {
|
||||
return (
|
||||
<LogoutLayout logoutMutation={logoutMutation}>
|
||||
You are currently logged in with the Tailscale integration identified by
|
||||
the <code>{tailscale.nodeName}</code> node. Click the button below to
|
||||
log out.
|
||||
<Trans
|
||||
i18nKey="logoutTailscaleSubtitle"
|
||||
t={t}
|
||||
components={{
|
||||
code: <code />,
|
||||
}}
|
||||
values={{
|
||||
deviceName: tailscale.nodeName,
|
||||
}}
|
||||
shouldUnescape={true}
|
||||
/>
|
||||
</LogoutLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -38,16 +37,17 @@ type Services struct {
|
||||
}
|
||||
|
||||
type BootstrapApp struct {
|
||||
config model.Config
|
||||
runtime model.RuntimeConfig
|
||||
services Services
|
||||
log *logger.Logger
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
queries *repository.Queries
|
||||
router *gin.Engine
|
||||
db *sql.DB
|
||||
wg sync.WaitGroup
|
||||
config model.Config
|
||||
runtime model.RuntimeConfig
|
||||
services Services
|
||||
log *logger.Logger
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
queries *repository.Queries
|
||||
router *gin.Engine
|
||||
db *sql.DB
|
||||
wg sync.WaitGroup
|
||||
listeners []Listener
|
||||
}
|
||||
|
||||
func NewBootstrapApp(config model.Config) *BootstrapApp {
|
||||
@@ -254,56 +254,32 @@ func (app *BootstrapApp) Setup() error {
|
||||
app.wg.Go(app.heartbeatRoutine)
|
||||
}
|
||||
|
||||
// create err channel to listen for server errors
|
||||
errChanLen := 0
|
||||
|
||||
// setup listeners
|
||||
runUnix := app.config.Server.SocketPath != ""
|
||||
runHTTP := app.config.Server.SocketPath == "" || app.config.Server.ConcurrentListenersEnabled
|
||||
runTailscale := app.services.tailscaleService != nil
|
||||
|
||||
if runUnix {
|
||||
errChanLen++
|
||||
if runHTTP {
|
||||
app.listeners = append(app.listeners, ListenerHTTP)
|
||||
}
|
||||
|
||||
if runHTTP {
|
||||
errChanLen++
|
||||
if runUnix {
|
||||
app.listeners = append(app.listeners, ListenerUnix)
|
||||
}
|
||||
|
||||
if runTailscale {
|
||||
errChanLen++
|
||||
app.listeners = append(app.listeners, ListenerTailscale)
|
||||
}
|
||||
|
||||
errChan := make(chan error, errChanLen)
|
||||
|
||||
if app.config.Server.ConcurrentListenersEnabled {
|
||||
app.log.App.Info().Msg("Concurrent listeners enabled, will run on all available listeners")
|
||||
}
|
||||
|
||||
// serve unix
|
||||
if runUnix {
|
||||
app.wg.Go(func() {
|
||||
if err := app.serveUnix(); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
})
|
||||
}
|
||||
// run listeners
|
||||
lec, err := app.runListeners()
|
||||
|
||||
// serve to http
|
||||
if runHTTP {
|
||||
app.wg.Go(func() {
|
||||
if err := app.serveHTTP(); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// serve to tailscale
|
||||
if runTailscale {
|
||||
app.wg.Go(func() {
|
||||
if err := app.serveTailscale(); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run listeners: %w", err)
|
||||
}
|
||||
|
||||
// monitor cancellation and server errors
|
||||
@@ -312,123 +288,14 @@ func (app *BootstrapApp) Setup() error {
|
||||
case <-app.ctx.Done():
|
||||
app.log.App.Info().Msg("Oh, it's time for me to go, bye!")
|
||||
return nil
|
||||
case err := <-errChan:
|
||||
case err := <-lec:
|
||||
if err != nil {
|
||||
return fmt.Errorf("server error: %w", err)
|
||||
return fmt.Errorf("listener error: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) serveHTTP() error {
|
||||
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
|
||||
|
||||
app.log.App.Info().Msgf("Starting server on %s", address)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: address,
|
||||
Handler: app.router.Handler(),
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-app.ctx.Done()
|
||||
app.log.App.Debug().Msg("Shutting down http listener")
|
||||
server.Shutdown(app.ctx)
|
||||
}()
|
||||
|
||||
err := server.ListenAndServe()
|
||||
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("failed to start http listener: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) serveUnix() error {
|
||||
if app.config.Server.SocketPath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := os.Stat(app.config.Server.SocketPath)
|
||||
|
||||
if err == nil {
|
||||
app.log.App.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath)
|
||||
err := os.Remove(app.config.Server.SocketPath)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove existing socket file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
app.log.App.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath)
|
||||
|
||||
listener, err := net.Listen("unix", app.config.Server.SocketPath)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create unix socket listener: %w", err)
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Handler: app.router.Handler(),
|
||||
}
|
||||
|
||||
shutdown := func() {
|
||||
server.Shutdown(app.ctx)
|
||||
listener.Close()
|
||||
os.Remove(app.config.Server.SocketPath)
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-app.ctx.Done()
|
||||
app.log.App.Debug().Msg("Shutting down unix socket listener")
|
||||
shutdown()
|
||||
}()
|
||||
|
||||
err = server.Serve(listener)
|
||||
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
shutdown()
|
||||
return fmt.Errorf("failed to start unix socket listener: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) serveTailscale() error {
|
||||
app.log.App.Info().Msgf("Starting Tailscale server on %s", app.services.tailscaleService.GetHostname())
|
||||
|
||||
listener, err := app.services.tailscaleService.CreateListener()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create tailscale listener: %w", err)
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Handler: app.router.Handler(),
|
||||
}
|
||||
|
||||
shutdown := func() {
|
||||
server.Shutdown(app.ctx)
|
||||
listener.Close()
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-app.ctx.Done()
|
||||
app.log.App.Debug().Msg("Shutting down Tailscale listener")
|
||||
shutdown()
|
||||
}()
|
||||
|
||||
err = server.Serve(listener)
|
||||
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
shutdown()
|
||||
return fmt.Errorf("failed to start tailscale listener: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) heartbeatRoutine() {
|
||||
ticker := time.NewTicker(time.Duration(12) * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||
"github.com/tinyauthapp/tinyauth/internal/middleware"
|
||||
@@ -9,6 +13,14 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Listener int
|
||||
|
||||
const (
|
||||
ListenerHTTP Listener = iota
|
||||
ListenerUnix
|
||||
ListenerTailscale
|
||||
)
|
||||
|
||||
func (app *BootstrapApp) setupRouter() error {
|
||||
// we don't want gin debug mode
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
@@ -53,3 +65,119 @@ func (app *BootstrapApp) setupRouter() error {
|
||||
app.router = engine
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) runListeners() (chan error, error) {
|
||||
// lec -> listener error channel
|
||||
lec := make(chan error, len(app.listeners))
|
||||
|
||||
for _, listenerType := range app.listeners {
|
||||
listenerFunc, err := app.listenerFromType(listenerType)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get listener function: %w", err)
|
||||
}
|
||||
|
||||
app.wg.Go(func() {
|
||||
lec <- listenerFunc()
|
||||
})
|
||||
}
|
||||
|
||||
return lec, nil
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) listenerFromType(listenerType Listener) (func() error, error) {
|
||||
switch listenerType {
|
||||
case ListenerHTTP:
|
||||
return app.serveHTTP, nil
|
||||
case ListenerUnix:
|
||||
return app.serveUnix, nil
|
||||
case ListenerTailscale:
|
||||
return app.serveTailscale, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid listener type: %d", listenerType)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) serveHTTP() error {
|
||||
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
|
||||
|
||||
app.log.App.Info().Msgf("Starting server on %s", address)
|
||||
|
||||
listener, err := net.Listen("tcp", address)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create tcp listener: %w", err)
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: address,
|
||||
Handler: app.router.Handler(),
|
||||
}
|
||||
|
||||
return app.serve(listener, server, "http")
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) serveUnix() error {
|
||||
_, err := os.Stat(app.config.Server.SocketPath)
|
||||
|
||||
if err == nil {
|
||||
app.log.App.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath)
|
||||
err := os.Remove(app.config.Server.SocketPath)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove existing socket file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
app.log.App.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath)
|
||||
|
||||
listener, err := net.Listen("unix", app.config.Server.SocketPath)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create unix socket listener: %w", err)
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Handler: app.router.Handler(),
|
||||
}
|
||||
|
||||
return app.serve(listener, server, "unix socket")
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) serveTailscale() error {
|
||||
app.log.App.Info().Msgf("Starting Tailscale server on %s", fmt.Sprintf("https://%s", app.services.tailscaleService.GetHostname()))
|
||||
|
||||
listener, err := app.services.tailscaleService.CreateListener()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create tailscale listener: %w", err)
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Handler: app.router.Handler(),
|
||||
}
|
||||
|
||||
return app.serve(listener, server, "tailscale")
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) serve(listener net.Listener, server *http.Server, name string) error {
|
||||
shutdown := func() {
|
||||
server.Shutdown(app.ctx)
|
||||
listener.Close()
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-app.ctx.Done()
|
||||
app.log.App.Debug().Msgf("Shutting down %s listener", name)
|
||||
shutdown()
|
||||
}()
|
||||
|
||||
err := server.Serve(listener)
|
||||
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
shutdown()
|
||||
return fmt.Errorf("failed to start %s listener: %w", name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func NewDefaultConfiguration() *Config {
|
||||
ConfigFile: "",
|
||||
},
|
||||
Tailscale: TailscaleConfig{
|
||||
Dir: "./state",
|
||||
Dir: "./tailscale_state",
|
||||
},
|
||||
LabelProvider: "auto",
|
||||
}
|
||||
@@ -206,6 +206,7 @@ type ExperimentalConfig struct {
|
||||
}
|
||||
|
||||
type TailscaleConfig struct {
|
||||
Enabled bool `description:"Enable Tailscale integration." yaml:"enabled"`
|
||||
Dir string `description:"Tailscale state directory." yaml:"dir"`
|
||||
Hostname string `description:"Tailscale hostname." yaml:"hostname"`
|
||||
AuthKey string `description:"Tailscale auth key." yaml:"authKey"`
|
||||
|
||||
@@ -27,6 +27,10 @@ type TailscaleService struct {
|
||||
}
|
||||
|
||||
func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Context, wg *sync.WaitGroup) (*TailscaleService, error) {
|
||||
if !config.Tailscale.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
srv := new(tsnet.Server)
|
||||
|
||||
// node options
|
||||
|
||||
Reference in New Issue
Block a user