mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-03-01 12:22:02 +00:00
Compare commits
6 Commits
v5.0.0-alp
...
ce25f9561f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce25f9561f | ||
|
|
f24595b24e | ||
|
|
285edba88c | ||
|
|
51d95fa455 | ||
|
|
fd16f91011 | ||
|
|
fb671139cd |
4
Makefile
4
Makefile
@@ -71,6 +71,10 @@ develop-infisical:
|
|||||||
prod:
|
prod:
|
||||||
docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
||||||
|
|
||||||
|
# Production - Infisical
|
||||||
|
prod-infisical:
|
||||||
|
infisical run --env=dev -- docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
||||||
|
|
||||||
# SQL
|
# SQL
|
||||||
.PHONY: sql
|
.PHONY: sql
|
||||||
sql:
|
sql:
|
||||||
|
|||||||
@@ -28,7 +28,20 @@ func healthcheckCmd() *cli.Command {
|
|||||||
Run: func(args []string) error {
|
Run: func(args []string) error {
|
||||||
tlog.NewSimpleLogger().Init()
|
tlog.NewSimpleLogger().Init()
|
||||||
|
|
||||||
appUrl := os.Getenv("TINYAUTH_APPURL")
|
appUrl := "http://127.0.0.1:3000"
|
||||||
|
|
||||||
|
appUrlEnv := os.Getenv("TINYAUTH_APPURL")
|
||||||
|
srvAddr := os.Getenv("TINYAUTH_SERVER_ADDRESS")
|
||||||
|
srvPort := os.Getenv("TINYAUTH_SERVER_PORT")
|
||||||
|
|
||||||
|
if appUrlEnv != "" {
|
||||||
|
appUrl = appUrlEnv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local-direct connection is preferred over the public app URL
|
||||||
|
if srvAddr != "" && srvPort != "" {
|
||||||
|
appUrl = fmt.Sprintf("http://%s:%s", srvAddr, srvPort)
|
||||||
|
}
|
||||||
|
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
appUrl = args[0]
|
appUrl = args[0]
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
@apply bg-accent border border-border rounded-md p-2;
|
@apply bg-accent border border-border rounded-md p-2 whitespace-break-spaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lead {
|
.lead {
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -11,6 +11,7 @@ require (
|
|||||||
github.com/charmbracelet/huh v0.8.0
|
github.com/charmbracelet/huh v0.8.0
|
||||||
github.com/docker/docker v28.5.2+incompatible
|
github.com/docker/docker v28.5.2+incompatible
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3
|
||||||
github.com/go-ldap/ldap/v3 v3.4.12
|
github.com/go-ldap/ldap/v3 v3.4.12
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||||
github.com/google/go-querystring v1.2.0
|
github.com/google/go-querystring v1.2.0
|
||||||
@@ -61,7 +62,6 @@ require (
|
|||||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
type BootstrapApp struct {
|
type BootstrapApp struct {
|
||||||
config config.Config
|
config config.Config
|
||||||
context struct {
|
context struct {
|
||||||
|
appUrl string
|
||||||
uuid string
|
uuid string
|
||||||
cookieDomain string
|
cookieDomain string
|
||||||
sessionCookieName string
|
sessionCookieName string
|
||||||
@@ -42,10 +43,20 @@ func NewBootstrapApp(config config.Config) *BootstrapApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *BootstrapApp) Setup() error {
|
func (app *BootstrapApp) Setup() error {
|
||||||
|
// get app url
|
||||||
|
appUrl, err := url.Parse(app.config.AppURL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
app.context.appUrl = appUrl.Scheme + "://" + appUrl.Host
|
||||||
|
|
||||||
// validate session config
|
// validate session config
|
||||||
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
|
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
|
||||||
return fmt.Errorf("session max lifetime cannot be less than session expiry")
|
return fmt.Errorf("session max lifetime cannot be less than session expiry")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse users
|
// Parse users
|
||||||
users, err := utils.GetUsers(app.config.Auth.Users, app.config.Auth.UsersFile)
|
users, err := utils.GetUsers(app.config.Auth.Users, app.config.Auth.UsersFile)
|
||||||
|
|
||||||
@@ -62,16 +73,12 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
secret := utils.GetSecret(provider.ClientSecret, provider.ClientSecretFile)
|
secret := utils.GetSecret(provider.ClientSecret, provider.ClientSecretFile)
|
||||||
provider.ClientSecret = secret
|
provider.ClientSecret = secret
|
||||||
provider.ClientSecretFile = ""
|
provider.ClientSecretFile = ""
|
||||||
app.context.oauthProviders[name] = provider
|
|
||||||
}
|
|
||||||
|
|
||||||
for id := range config.OverrideProviders {
|
if provider.RedirectURL == "" {
|
||||||
if provider, exists := app.context.oauthProviders[id]; exists {
|
provider.RedirectURL = app.context.appUrl + "/api/oauth/callback/" + name
|
||||||
if provider.RedirectURL == "" {
|
|
||||||
provider.RedirectURL = app.config.AppURL + "/api/oauth/callback/" + id
|
|
||||||
app.context.oauthProviders[id] = provider
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.context.oauthProviders[name] = provider
|
||||||
}
|
}
|
||||||
|
|
||||||
for id, provider := range app.context.oauthProviders {
|
for id, provider := range app.context.oauthProviders {
|
||||||
@@ -92,7 +99,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get cookie domain
|
// Get cookie domain
|
||||||
cookieDomain, err := utils.GetCookieDomain(app.config.AppURL)
|
cookieDomain, err := utils.GetCookieDomain(app.context.appUrl)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -101,7 +108,6 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
app.context.cookieDomain = cookieDomain
|
app.context.cookieDomain = cookieDomain
|
||||||
|
|
||||||
// Cookie names
|
// Cookie names
|
||||||
appUrl, _ := url.Parse(app.config.AppURL) // Already validated
|
|
||||||
app.context.uuid = utils.GenerateUUID(appUrl.Hostname())
|
app.context.uuid = utils.GenerateUUID(appUrl.Hostname())
|
||||||
cookieId := strings.Split(app.context.uuid, "-")[0]
|
cookieId := strings.Split(app.context.uuid, "-")[0]
|
||||||
app.context.sessionCookieName = fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId)
|
app.context.sessionCookieName = fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package bootstrap
|
|||||||
import (
|
import (
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Services struct {
|
type Services struct {
|
||||||
@@ -31,7 +32,8 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
err := ldapService.Init()
|
err := ldapService.Init()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Services{}, err
|
tlog.App.Warn().Err(err).Msg("Failed to setup LDAP service, starting without it")
|
||||||
|
ldapService.Unconfigure()
|
||||||
}
|
}
|
||||||
|
|
||||||
services.ldapService = ldapService
|
services.ldapService = ldapService
|
||||||
|
|||||||
@@ -97,6 +97,11 @@ func (controller *OIDCController) GetClientInfo(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (controller *OIDCController) Authorize(c *gin.Context) {
|
func (controller *OIDCController) Authorize(c *gin.Context) {
|
||||||
|
if !controller.oidc.IsConfigured() {
|
||||||
|
controller.authorizeError(c, errors.New("err_oidc_not_configured"), "OIDC not configured", "This instance is not configured for OIDC", "", "", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
userContext, err := utils.GetContext(c)
|
userContext, err := utils.GetContext(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -177,6 +182,14 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (controller *OIDCController) Token(c *gin.Context) {
|
func (controller *OIDCController) Token(c *gin.Context) {
|
||||||
|
if !controller.oidc.IsConfigured() {
|
||||||
|
tlog.App.Warn().Msg("OIDC not configured")
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"error": "not_found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var req TokenRequest
|
var req TokenRequest
|
||||||
|
|
||||||
err := c.Bind(&req)
|
err := c.Bind(&req)
|
||||||
@@ -306,6 +319,14 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (controller *OIDCController) Userinfo(c *gin.Context) {
|
func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||||
|
if !controller.oidc.IsConfigured() {
|
||||||
|
tlog.App.Warn().Msg("OIDC not configured")
|
||||||
|
c.JSON(404, gin.H{
|
||||||
|
"error": "not_found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
authorization := c.GetHeader("Authorization")
|
authorization := c.GetHeader("Authorization")
|
||||||
|
|
||||||
tokenType, token, ok := strings.Cut(authorization, " ")
|
tokenType, token, ok := strings.Cut(authorization, " ")
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
@@ -114,8 +113,8 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
|
|
||||||
err := controller.auth.CreateSessionCookie(c, &repository.Session{
|
err := controller.auth.CreateSessionCookie(c, &repository.Session{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Name: utils.Capitalize(req.Username),
|
Name: utils.Capitalize(user.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain),
|
Email: utils.CompileUserEmail(user.Username, controller.config.CookieDomain),
|
||||||
Provider: "local",
|
Provider: "local",
|
||||||
TotpPending: true,
|
TotpPending: true,
|
||||||
})
|
})
|
||||||
@@ -141,7 +140,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
sessionCookie := repository.Session{
|
sessionCookie := repository.Session{
|
||||||
Username: req.Username,
|
Username: req.Username,
|
||||||
Name: utils.Capitalize(req.Username),
|
Name: utils.Capitalize(req.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain),
|
Email: utils.CompileUserEmail(req.Username, controller.config.CookieDomain),
|
||||||
Provider: "local",
|
Provider: "local",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +254,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
sessionCookie := repository.Session{
|
sessionCookie := repository.Session{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Name: utils.Capitalize(user.Username),
|
Name: utils.Capitalize(user.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.config.CookieDomain),
|
Email: utils.CompileUserEmail(user.Username, controller.config.CookieDomain),
|
||||||
Provider: "local",
|
Provider: "local",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -186,7 +185,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
c.Set("context", &config.UserContext{
|
c.Set("context", &config.UserContext{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Name: utils.Capitalize(user.Username),
|
Name: utils.Capitalize(user.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.config.CookieDomain),
|
Email: utils.CompileUserEmail(user.Username, m.config.CookieDomain),
|
||||||
Provider: "local",
|
Provider: "local",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
TotpEnabled: user.TotpSecret != "",
|
TotpEnabled: user.TotpSecret != "",
|
||||||
@@ -208,7 +207,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
c.Set("context", &config.UserContext{
|
c.Set("context", &config.UserContext{
|
||||||
Username: basic.Username,
|
Username: basic.Username,
|
||||||
Name: utils.Capitalize(basic.Username),
|
Name: utils.Capitalize(basic.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.config.CookieDomain),
|
Email: utils.CompileUserEmail(basic.Username, m.config.CookieDomain),
|
||||||
Provider: "ldap",
|
Provider: "ldap",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
LdapGroups: strings.Join(ldapUser.Groups, ","),
|
LdapGroups: strings.Join(ldapUser.Groups, ","),
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ type LdapServiceConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LdapService struct {
|
type LdapService struct {
|
||||||
config LdapServiceConfig
|
config LdapServiceConfig
|
||||||
conn *ldapgo.Conn
|
conn *ldapgo.Conn
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
cert *tls.Certificate
|
cert *tls.Certificate
|
||||||
|
isConfigured bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLdapService(config LdapServiceConfig) *LdapService {
|
func NewLdapService(config LdapServiceConfig) *LdapService {
|
||||||
@@ -36,16 +37,33 @@ func NewLdapService(config LdapServiceConfig) *LdapService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If you have an ldap address then you must need ldap
|
|
||||||
func (ldap *LdapService) IsConfigured() bool {
|
func (ldap *LdapService) IsConfigured() bool {
|
||||||
return ldap.config.Address != ""
|
return ldap.isConfigured
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ldap *LdapService) Unconfigure() error {
|
||||||
|
if !ldap.isConfigured {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ldap.conn != nil {
|
||||||
|
if err := ldap.conn.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close LDAP connection: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ldap.isConfigured = false
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ldap *LdapService) Init() error {
|
func (ldap *LdapService) Init() error {
|
||||||
if !ldap.IsConfigured() {
|
if ldap.config.Address == "" {
|
||||||
|
ldap.isConfigured = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ldap.isConfigured = true
|
||||||
|
|
||||||
// Check whether authentication with client certificate is possible
|
// Check whether authentication with client certificate is possible
|
||||||
if ldap.config.AuthCert != "" && ldap.config.AuthKey != "" {
|
if ldap.config.AuthCert != "" && ldap.config.AuthKey != "" {
|
||||||
cert, err := tls.LoadX509KeyPair(ldap.config.AuthCert, ldap.config.AuthKey)
|
cert, err := tls.LoadX509KeyPair(ldap.config.AuthCert, ldap.config.AuthKey)
|
||||||
|
|||||||
@@ -83,12 +83,13 @@ type OIDCServiceConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OIDCService struct {
|
type OIDCService struct {
|
||||||
config OIDCServiceConfig
|
config OIDCServiceConfig
|
||||||
queries *repository.Queries
|
queries *repository.Queries
|
||||||
clients map[string]config.OIDCClientConfig
|
clients map[string]config.OIDCClientConfig
|
||||||
privateKey *rsa.PrivateKey
|
privateKey *rsa.PrivateKey
|
||||||
publicKey crypto.PublicKey
|
publicKey crypto.PublicKey
|
||||||
issuer string
|
issuer string
|
||||||
|
isConfigured bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOIDCService(config OIDCServiceConfig, queries *repository.Queries) *OIDCService {
|
func NewOIDCService(config OIDCServiceConfig, queries *repository.Queries) *OIDCService {
|
||||||
@@ -98,9 +99,19 @@ func NewOIDCService(config OIDCServiceConfig, queries *repository.Queries) *OIDC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: A cleanup routine is needed to clean up expired tokens/code/userinfo
|
func (service *OIDCService) IsConfigured() bool {
|
||||||
|
return service.isConfigured
|
||||||
|
}
|
||||||
|
|
||||||
func (service *OIDCService) Init() error {
|
func (service *OIDCService) Init() error {
|
||||||
|
// If not configured, skip init
|
||||||
|
if len(service.config.Clients) == 0 {
|
||||||
|
service.isConfigured = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
service.isConfigured = true
|
||||||
|
|
||||||
// Ensure issuer is https
|
// Ensure issuer is https
|
||||||
uissuer, err := url.Parse(service.config.Issuer)
|
uissuer, err := url.Parse(service.config.Issuer)
|
||||||
|
|
||||||
@@ -207,6 +218,7 @@ func (service *OIDCService) Init() error {
|
|||||||
}
|
}
|
||||||
client.ClientSecretFile = ""
|
client.ClientSecretFile = ""
|
||||||
service.clients[id] = client
|
service.clients[id] = client
|
||||||
|
tlog.App.Info().Str("id", client.ID).Msg("Registered OIDC client")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -49,3 +49,11 @@ func TestCoalesceToString(t *testing.T) {
|
|||||||
// Test with nil input
|
// Test with nil input
|
||||||
assert.Equal(t, "", utils.CoalesceToString(nil))
|
assert.Equal(t, "", utils.CoalesceToString(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCompileUserEmail(t *testing.T) {
|
||||||
|
// Test with valid email
|
||||||
|
assert.Equal(t, "user@example.com", utils.CompileUserEmail("user@example.com", "example.com"))
|
||||||
|
|
||||||
|
// Test with invalid email
|
||||||
|
assert.Equal(t, "user@example.com", utils.CompileUserEmail("user", "example.com"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
@@ -90,3 +92,13 @@ func ParseUser(userStr string) (config.User, error) {
|
|||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CompileUserEmail(username string, domain string) string {
|
||||||
|
_, err := mail.ParseAddress(username)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("%s@%s", strings.ToLower(username), domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
return username
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user