diff --git a/cmd/agent/docker/main.go b/cmd/agent/docker/main.go index 89c1b489..c5cf4985 100644 --- a/cmd/agent/docker/main.go +++ b/cmd/agent/docker/main.go @@ -16,10 +16,11 @@ import ( ) var ( - interval = 5 * time.Second - logger = util.Require(zap.NewDevelopment()) - client = util.Require(agentclient.NewFromEnv(logger)) - agentVersionID = os.Getenv("DISTR_AGENT_VERSION_ID") + interval = 5 * time.Second + logger = util.Require(zap.NewDevelopment()) + client = util.Require(agentclient.NewFromEnv(logger)) + agentVersionID = os.Getenv("DISTR_AGENT_VERSION_ID") + distrRegistryHost = os.Getenv("DISTR_REGISTRY_HOST") ) func init() { @@ -118,7 +119,7 @@ loop: var agentDeployment *AgentDeployment var status string - _, err = agentauth.EnsureAuth(ctx, resource.Deployment.AgentDeployment) + _, err = agentauth.EnsureAuth(ctx, distrRegistryHost, client.RawToken(), resource.Deployment.AgentDeployment) if err != nil { logger.Error("docker auth error", zap.Error(err)) } else if agentDeployment, status, err = ApplyComposeFile(ctx, *resource.Deployment); err == nil { diff --git a/cmd/agent/kubernetes/helm_actions.go b/cmd/agent/kubernetes/helm_actions.go index b70f9a7b..8e80707f 100644 --- a/cmd/agent/kubernetes/helm_actions.go +++ b/cmd/agent/kubernetes/helm_actions.go @@ -32,7 +32,8 @@ func GetHelmActionConfig( var cfg action.Configuration if deployment != nil { - if authorizer, err := agentauth.EnsureAuth(ctx, *deployment); err != nil { + if authorizer, err := + agentauth.EnsureAuth(ctx, distrRegistryHost, agentClient.RawToken(), *deployment); err != nil { return nil, err } else if rc, err := registry.NewClient(registry.ClientOptAuthorizer(authorizer)); err != nil { return nil, err diff --git a/cmd/agent/kubernetes/main.go b/cmd/agent/kubernetes/main.go index 3c12ff76..6e40ec93 100644 --- a/cmd/agent/kubernetes/main.go +++ b/cmd/agent/kubernetes/main.go @@ -22,14 +22,15 @@ import ( ) var ( - interval = 5 * time.Second - logger = util.Require(zap.NewDevelopment()) - agentClient = util.Require(agentclient.NewFromEnv(logger)) - k8sConfigFlags = genericclioptions.NewConfigFlags(true) - k8sClient = util.Require(kubernetes.NewForConfig(util.Require(k8sConfigFlags.ToRESTConfig()))) - k8sDynamicClient = util.Require(dynamic.NewForConfig(util.Require(k8sConfigFlags.ToRESTConfig()))) - k8sRestMapper = util.Require(k8sConfigFlags.ToRESTMapper()) - agentVersionId = os.Getenv("DISTR_AGENT_VERSION_ID") + interval = 5 * time.Second + logger = util.Require(zap.NewDevelopment()) + agentClient = util.Require(agentclient.NewFromEnv(logger)) + k8sConfigFlags = genericclioptions.NewConfigFlags(true) + k8sClient = util.Require(kubernetes.NewForConfig(util.Require(k8sConfigFlags.ToRESTConfig()))) + k8sDynamicClient = util.Require(dynamic.NewForConfig(util.Require(k8sConfigFlags.ToRESTConfig()))) + k8sRestMapper = util.Require(k8sConfigFlags.ToRESTMapper()) + agentVersionId = os.Getenv("DISTR_AGENT_VERSION_ID") + distrRegistryHost = os.Getenv("DISTR_REGISTRY_HOST") ) func init() { diff --git a/internal/agentauth/auth.go b/internal/agentauth/auth.go index 197655b2..32bf4dc6 100644 --- a/internal/agentauth/auth.go +++ b/internal/agentauth/auth.go @@ -17,7 +17,11 @@ import ( var previousAuth = map[uuid.UUID]map[string]api.AgentRegistryAuth{} var authClients = map[uuid.UUID]auth.Client{} -func EnsureAuth(ctx context.Context, deployment api.AgentDeployment) (auth.Client, error) { +func EnsureAuth( + ctx context.Context, + distrRegistryHost, jwt string, + deployment api.AgentDeployment, +) (auth.Client, error) { if err := os.MkdirAll(DockerConfigDir(deployment), 0o700); err != nil { return nil, fmt.Errorf("could not create docker config dir for deployment: %w", err) } @@ -32,6 +36,16 @@ func EnsureAuth(ctx context.Context, deployment api.AgentDeployment) (auth.Clien authClients[deployment.ID] = c client = c } + + if distrRegistryHost != "" { + client.LoginWithOpts( + auth.WithLoginContext(ctx), + auth.WithLoginInsecure(), + auth.WithLoginHostname(distrRegistryHost), + auth.WithLoginUsername("unused"), + auth.WithLoginSecret(jwt), + ) + } } if !maps.Equal(previousAuth[deployment.ID], deployment.RegistryAuth) { diff --git a/internal/agentclient/client.go b/internal/agentclient/client.go index ccdd91c4..0db27ff1 100644 --- a/internal/agentclient/client.go +++ b/internal/agentclient/client.go @@ -153,6 +153,10 @@ func (c *Client) HasTokenExpiredAfter(t time.Time) bool { return c.token == nil || c.token.Expiration().Before(t) } +func (c *Client) RawToken() string { + return c.rawToken +} + func (c *Client) doAuthenticated(ctx context.Context, r *http.Request) (*http.Response, error) { if err := c.EnsureToken(ctx); err != nil { return nil, err diff --git a/internal/agentmanifest/common.go b/internal/agentmanifest/common.go index 081b77e5..c8127395 100644 --- a/internal/agentmanifest/common.go +++ b/internal/agentmanifest/common.go @@ -48,6 +48,7 @@ func getTemplateData( ) map[string]any { result := map[string]any{ "agentInterval": env.AgentInterval(), + "registryHost": env.RegistryHost(), "agentDockerConfig": base64.StdEncoding.EncodeToString(env.AgentDockerConfig()), "agentVersion": deploymentTarget.AgentVersion.Name, "agentVersionId": deploymentTarget.AgentVersion.ID, diff --git a/internal/auth/authentication.go b/internal/auth/authentication.go index 8421ae99..0d579b1b 100644 --- a/internal/auth/authentication.go +++ b/internal/auth/authentication.go @@ -44,14 +44,25 @@ var AgentAuthentication = authn.New( // ArtifactsAuthentication supports Basic auth login for OCI clients, where the password should be a PAT. // The given PAT is verified against the database, to make sure that the user still exists. var ArtifactsAuthentication = authn.New( - authn.Chain4( + authn.Chain( token.NewExtractor( token.WithExtractorFuncs(token.FromBasicAuth()), - token.WithErrorHeaders(map[string]string{"WWW-Authenticate": "Basic realm=\"Distr\""}), + token.WithErrorHeaders(http.Header{"WWW-Authenticate": []string{"Basic realm=\"Distr\""}}), + ), + authn.Alternative( + // Auhtenticate UserAccount with PAT + authn.Chain3( + authkey.Authenticator(), + authinfo.AuthKeyAuthenticator(), + authinfo.DbAuthenticator(), + ), + // Authenticate with Agent JWT + authn.Chain3( + jwt.Authenticator(authjwt.JWTAuth), + authinfo.AgentJWTAuthenticator(), + authinfo.AgentDbAuthenticator(), + ), ), - authkey.Authenticator(), - authinfo.AuthKeyAuthenticator(), - authinfo.DbAuthenticator(), ), ) diff --git a/internal/authn/agent/authenticator.go b/internal/authn/agent/authenticator.go new file mode 100644 index 00000000..d53ff9f3 --- /dev/null +++ b/internal/authn/agent/authenticator.go @@ -0,0 +1,78 @@ +package agent + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "github.com/glasskube/distr/internal/authjwt" + "github.com/glasskube/distr/internal/authn" + "github.com/glasskube/distr/internal/authn/authinfo" + "github.com/glasskube/distr/internal/authn/basic" + "github.com/glasskube/distr/internal/db" + "github.com/glasskube/distr/internal/security" + "github.com/glasskube/distr/internal/types" + "github.com/google/uuid" +) + +func Authenticator() authn.Authenticator[basic.Auth, authinfo.AuthInfo] { + fn := func(ctx context.Context, basic basic.Auth) (out authinfo.AuthInfo, err error) { + if targetID, err1 := uuid.Parse(basic.Username); err1 != nil { + err = fmt.Errorf("%w: %w", authn.ErrBadAuthentication, err1) + } else if target, err1 := db.GetDeploymentTarget(ctx, targetID, nil); err1 != nil { + err = err1 + } else if target.AccessKeyHash == nil || target.AccessKeySalt == nil { + err = errors.New("access key or salt is nil") + } else if err1 := + security.VerifyAccessKey(*target.AccessKeySalt, *target.AccessKeyHash, basic.Password); err1 != nil { + err = fmt.Errorf("%w: %w", authn.ErrBadAuthentication, err1) + } else if user, err1 := db.GetUserAccountByID(ctx, target.CreatedByUserAccountID); err1 != nil { + err = err1 + } else if orgs, err1 := db.GetOrganizationsForUser(ctx, user.ID); err1 != nil { + err = err1 + } else if len(orgs) != 1 { + err = fmt.Errorf("user must have exactly one organization") + } else if orgs[0].UserRole != types.UserRoleCustomer { + err = fmt.Errorf("user must have role customer") + } else if _, token, err1 := authjwt.GenerateDefaultToken(*user, orgs[0]); err1 != nil { + err = err1 + } else { + err = &tokenError{Token: token} + } + return + } + return authn.AuthenticatorFunc[basic.Auth, authinfo.AuthInfo](fn) +} + +type tokenError struct { + Token string `json:"token"` +} + +var _ authn.WithResponseHeaders = &tokenError{} +var _ authn.WithResponseStatus = &tokenError{} +var _ authn.ResponseBodyWriter = &tokenError{} + +// Error implements error. +func (t *tokenError) Error() string { + return "NOT AN ERROR: agent successful token auth" // :^) +} + +// ResponseHeaders implements authn.WithResponseHeaders. +func (t *tokenError) ResponseHeaders() http.Header { + result := http.Header{} + result.Add("Content-Type", "application/json") + return result +} + +// ResponseStatus implements authn.WithResponseStatus. +func (t *tokenError) ResponseStatus() int { + return http.StatusOK +} + +// WriteResponse implements authn.ResponseBodyWriter. +func (t *tokenError) WriteResponse(w io.Writer) { + _ = json.NewEncoder(w).Encode(t) +} diff --git a/internal/authn/authentication.go b/internal/authn/authentication.go index 106c9072..4e2f7f2a 100644 --- a/internal/authn/authentication.go +++ b/internal/authn/authentication.go @@ -4,6 +4,8 @@ import ( "context" "errors" "net/http" + + "go.uber.org/multierr" ) type contextKey struct{} @@ -76,16 +78,34 @@ func (a *Authentication[T]) ValidatorMiddleware(fn func(value T) error) func(nex } func (a *Authentication[T]) handleError(w http.ResponseWriter, r *http.Request, err error) { - hhe := &HttpHeaderError{} - if errors.As(err, &hhe) { - hhe.WriteTo(w) + for _, err := range multierr.Errors(err) { + var rh WithResponseHeaders + if errors.As(err, &rh) { + for key, value := range rh.ResponseHeaders() { + for _, v := range value { + w.Header().Add(key, v) + } + } + } } - if errors.Is(err, ErrBadAuthentication) || errors.Is(err, ErrNoAuthentication) { - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - } else if a.unknownErrorHandler == nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } else { + statusCode := http.StatusInternalServerError + + var rs WithResponseStatus + if errors.As(err, &rs) { + statusCode = rs.ResponseStatus() + } else if errors.Is(err, ErrBadAuthentication) || errors.Is(err, ErrNoAuthentication) { + statusCode = http.StatusUnauthorized + } else if a.unknownErrorHandler != nil { a.unknownErrorHandler(w, r, err) + return + } + + var rw ResponseBodyWriter + if errors.As(err, &rw) { + w.WriteHeader(statusCode) + rw.WriteResponse(w) + } else { + http.Error(w, http.StatusText(statusCode), statusCode) } } diff --git a/internal/authn/authenticator.go b/internal/authn/authenticator.go index 7421d574..e852a48a 100644 --- a/internal/authn/authenticator.go +++ b/internal/authn/authenticator.go @@ -2,7 +2,10 @@ package authn import ( "context" + "errors" "net/http" + + "go.uber.org/multierr" ) type Authenticator[IN any, OUT any] interface { @@ -61,3 +64,23 @@ func Chain4[IN any, MID1 any, MID2 any, MID3 any, OUT any]( } }) } + +func Alternative[A any, B any](authenticators ...Authenticator[A, B]) Authenticator[A, B] { + return AuthenticatorFunc[A, B](func(ctx context.Context, in A) (result B, err error) { + for _, authenticator := range authenticators { + if out, err1 := authenticator.Authenticate(ctx, in); err1 != nil { + multierr.AppendInto(&err, err1) + if errors.Is(err, ErrNoAuthentication) || errors.Is(err, ErrBadAuthentication) { + continue + } else { + break + } + } else { + result = out + err = nil + break + } + } + return + }) +} diff --git a/internal/authn/authinfo/db.go b/internal/authn/authinfo/db.go index 7e30febe..bdb33e47 100644 --- a/internal/authn/authinfo/db.go +++ b/internal/authn/authinfo/db.go @@ -8,6 +8,7 @@ import ( "github.com/glasskube/distr/internal/authn" "github.com/glasskube/distr/internal/db" "github.com/glasskube/distr/internal/types" + "github.com/glasskube/distr/internal/util" ) type DbAuthInfo struct { @@ -48,3 +49,26 @@ func DbAuthenticator() authn.Authenticator[AuthInfo, *DbAuthInfo] { }, nil }) } + +func AgentDbAuthenticator() authn.Authenticator[AgentAuthInfo, *DbAuthInfo] { + return authn.AuthenticatorFunc[AgentAuthInfo, *DbAuthInfo](func(ctx context.Context, a AgentAuthInfo) (*DbAuthInfo, error) { + userWithRole, org, err := db.GetUserAccountAndOrgForDeploymentTarget(ctx, a.CurrentDeploymentTargetID()) + if errors.Is(err, apierrors.ErrNotFound) { + return nil, authn.ErrBadAuthentication + } else if err != nil { + return nil, err + } + return &DbAuthInfo{ + AuthInfo: &SimpleAuthInfo{ + organizationID: &org.ID, + userID: userWithRole.ID, + userEmail: userWithRole.Email, + emailVerified: userWithRole.EmailVerifiedAt != nil, + userRole: util.PtrTo(userWithRole.UserRole), + rawToken: a.Token(), + }, + user: util.PtrTo(userWithRole.AsUserAccount()), + org: org, + }, nil + }) +} diff --git a/internal/authn/basic/authenticator.go b/internal/authn/basic/authenticator.go new file mode 100644 index 00000000..7bf2fd31 --- /dev/null +++ b/internal/authn/basic/authenticator.go @@ -0,0 +1,35 @@ +package basic + +import ( + "context" + "net/http" + + "github.com/glasskube/distr/internal/authn" +) + +type Auth struct { + Username, Password string +} + +func Authenticator() authn.RequestAuthenticator[Auth] { + fn := func(ctx context.Context, r *http.Request) (result Auth, err error) { + if username, password, ok := r.BasicAuth(); !ok { + err = authn.NewHttpHeaderError( + authn.ErrNoAuthentication, + http.Header{ + "WWW-Authenticate": []string{`Bearer realm="http://localhost:8585/v2/",service="localhost:8585"`, `Basic realm="Distr"`}, + }, + ) + } else { + result = Auth{Username: username, Password: password} + } + return + } + return authn.AuthenticatorFunc[*http.Request, Auth](fn) +} + +func Password() authn.Authenticator[Auth, string] { + return authn.AuthenticatorFunc[Auth, string](func(ctx context.Context, basic Auth) (string, error) { + return basic.Password, nil + }) +} diff --git a/internal/authn/errors.go b/internal/authn/errors.go index 2ab5a53b..bd1579ce 100644 --- a/internal/authn/errors.go +++ b/internal/authn/errors.go @@ -15,10 +15,17 @@ var ErrBadAuthentication = errors.New("bad authentication") type HttpHeaderError struct { wrapped error - headers map[string]string + headers http.Header } -func NewHttpHeaderError(err error, headers map[string]string) error { +// ResponseHEaders implements WithResponseHeaders. +func (err *HttpHeaderError) ResponseHeaders() http.Header { + return err.headers +} + +var _ WithResponseHeaders = &HttpHeaderError{} + +func NewHttpHeaderError(err error, headers http.Header) error { return &HttpHeaderError{ wrapped: err, headers: headers, @@ -32,9 +39,3 @@ func (err *HttpHeaderError) Error() string { func (err *HttpHeaderError) Unwrap() error { return err.wrapped } - -func (err *HttpHeaderError) WriteTo(w http.ResponseWriter) { - for k, v := range err.headers { - w.Header().Set(k, v) - } -} diff --git a/internal/authn/response.go b/internal/authn/response.go new file mode 100644 index 00000000..dd87f611 --- /dev/null +++ b/internal/authn/response.go @@ -0,0 +1,18 @@ +package authn + +import ( + "io" + "net/http" +) + +type WithResponseHeaders interface { + ResponseHeaders() http.Header +} + +type WithResponseStatus interface { + ResponseStatus() int +} + +type ResponseBodyWriter interface { + WriteResponse(w io.Writer) +} diff --git a/internal/authn/token/token.go b/internal/authn/token/token.go index dbd22958..1eb4f5c2 100644 --- a/internal/authn/token/token.go +++ b/internal/authn/token/token.go @@ -12,7 +12,7 @@ type TokenExtractorFunc func(r *http.Request) string type TokenExtractor struct { fns []TokenExtractorFunc - headers map[string]string + headers http.Header } // Authenticate implements Provider. @@ -35,7 +35,7 @@ func WithExtractorFuncs(fns ...TokenExtractorFunc) ExtractorOption { } } -func WithErrorHeaders(headers map[string]string) ExtractorOption { +func WithErrorHeaders(headers http.Header) ExtractorOption { return func(te *TokenExtractor) { te.headers = headers } diff --git a/internal/db/organization.go b/internal/db/organization.go index 281d2381..cf0d0f77 100644 --- a/internal/db/organization.go +++ b/internal/db/organization.go @@ -58,7 +58,7 @@ func UpdateOrganization(ctx context.Context, org *types.Organization) error { } } -func GetOrganizationsForUser(ctx context.Context, userID uuid.UUID) ([]*types.OrganizationWithUserRole, error) { +func GetOrganizationsForUser(ctx context.Context, userID uuid.UUID) ([]types.OrganizationWithUserRole, error) { db := internalctx.GetDb(ctx) rows, err := db.Query(ctx, ` SELECT`+organizationOutputExpr+`, j.user_role @@ -70,7 +70,7 @@ func GetOrganizationsForUser(ctx context.Context, userID uuid.UUID) ([]*types.Or if err != nil { return nil, err } - result, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[types.OrganizationWithUserRole]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[types.OrganizationWithUserRole]) if err != nil { return nil, err } else { diff --git a/internal/db/user_accounts.go b/internal/db/user_accounts.go index 65f4c549..185d250a 100644 --- a/internal/db/user_accounts.go +++ b/internal/db/user_accounts.go @@ -241,7 +241,8 @@ func GetUserAccountWithRole(ctx context.Context, userID, orgID uuid.UUID) (*type func GetUserAccountAndOrg(ctx context.Context, userID, orgID uuid.UUID, role types.UserRole) ( *types.UserAccount, - *types.Organization, error, + *types.Organization, + error, ) { db := internalctx.GetDb(ctx) rows, err := db.Query(ctx, @@ -271,6 +272,40 @@ func GetUserAccountAndOrg(ctx context.Context, userID, orgID uuid.UUID, role typ } } +func GetUserAccountAndOrgForDeploymentTarget( + ctx context.Context, + id uuid.UUID, +) (*types.UserAccountWithUserRole, *types.Organization, error) { + db := internalctx.GetDb(ctx) + rows, err := db.Query(ctx, + "SELECT ("+userAccountWithRoleOutputExpr+`), + (`+organizationOutputExpr+`) + FROM DeploymentTarget dt + JOIN Organization o ON o.id = dt.organization_id + JOIN UserAccount u ON u.id = dt.created_by_user_account_id + JOIN Organization_UserAccount j ON u.id = j.user_account_id + AND o.id = j.organization_id + WHERE dt.id = @id`, + pgx.NamedArgs{"id": id}, + ) + if err != nil { + return nil, nil, err + } + res, err := pgx.CollectExactlyOneRow[struct { + User types.UserAccountWithUserRole + Org types.Organization + }](rows, pgx.RowToStructByPos) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, nil, apierrors.ErrNotFound + } else { + return nil, nil, fmt.Errorf("could not map user or org: %w", err) + } + } else { + return &res.User, &res.Org, nil + } +} + func UpdateUserAccountLastLoggedIn(ctx context.Context, userID uuid.UUID) error { db := internalctx.GetDb(ctx) cmd, err := db.Exec( diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index ba709c66..15cadbf1 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -71,7 +71,7 @@ func authLoginHandler(w http.ResponseWriter, r *http.Request) { } org := orgs[0] - if _, tokenString, err := authjwt.GenerateDefaultToken(*user, *org); err != nil { + if _, tokenString, err := authjwt.GenerateDefaultToken(*user, org); err != nil { return fmt.Errorf("token creation failed: %w", err) } else if err = db.UpdateUserAccountLastLoggedIn(ctx, user.ID); err != nil { return err diff --git a/internal/resources/embedded/agent/docker/v1/docker-compose.yaml b/internal/resources/embedded/agent/docker/v1/docker-compose.yaml index 18006c77..e8384602 100644 --- a/internal/resources/embedded/agent/docker/v1/docker-compose.yaml +++ b/internal/resources/embedded/agent/docker/v1/docker-compose.yaml @@ -14,6 +14,7 @@ services: DISTR_INTERVAL: '{{ .agentInterval }}' DISTR_AGENT_VERSION_ID: '{{ .agentVersionId }}' DISTR_AGENT_SCRATCH_DIR: /scratch + DISTR_REGISTRY_HOST: '{{ .registryHost }}' HOST_DOCKER_CONFIG_DIR: ${HOST_DOCKER_CONFIG_DIR-${HOME}/.docker} volumes: - /var/run/docker.sock:/var/run/docker.sock diff --git a/internal/resources/embedded/agent/kubernetes/v1/manifest.yaml.tmpl b/internal/resources/embedded/agent/kubernetes/v1/manifest.yaml.tmpl index bce2ad42..782c0fd6 100644 --- a/internal/resources/embedded/agent/kubernetes/v1/manifest.yaml.tmpl +++ b/internal/resources/embedded/agent/kubernetes/v1/manifest.yaml.tmpl @@ -11,6 +11,7 @@ data: DISTR_STATUS_ENDPOINT: "{{ .statusEndpoint }}" DISTR_INTERVAL: "{{ .agentInterval }}" DISTR_AGENT_VERSION_ID: "{{ .agentVersionId }}" + DISTR_REGISTRY_HOST: "{{ .registryHost }}" {{ if .targetSecret }} --- diff --git a/internal/types/user_account.go b/internal/types/user_account.go index d4f30c28..39c219ba 100644 --- a/internal/types/user_account.go +++ b/internal/types/user_account.go @@ -1,8 +1,10 @@ package types import ( + "slices" "time" + "github.com/glasskube/distr/internal/util" "github.com/google/uuid" ) @@ -28,4 +30,18 @@ type UserAccountWithUserRole struct { Name string `db:"name" json:"name,omitempty"` UserRole UserRole `db:"user_role" json:"userRole"` // not copy+pasted Password string `db:"-" json:"-"` + // Don't forget to update AsUserAccount when adding fields! +} + +func (u *UserAccountWithUserRole) AsUserAccount() UserAccount { + return UserAccount{ + ID: u.ID, + CreatedAt: u.CreatedAt, + Email: u.Email, + EmailVerifiedAt: util.PtrCopy(u.EmailVerifiedAt), + PasswordHash: slices.Clone(u.PasswordHash), + PasswordSalt: slices.Clone(u.PasswordSalt), + Name: u.Name, + Password: u.Password, + } } diff --git a/internal/util/pointer.go b/internal/util/pointer.go index fba629c1..bc8dc378 100644 --- a/internal/util/pointer.go +++ b/internal/util/pointer.go @@ -3,3 +3,10 @@ package util func PtrTo[T any](value T) *T { return &value } + +func PtrCopy[T any](ptr *T) *T { + if ptr == nil { + return nil + } + return &*ptr +}