Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions admin/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,7 @@ type User struct {
QuotaTrialOrgs int `db:"quota_trial_orgs"`
CurrentTrialOrgsCount int `db:"current_trial_orgs_count"`
PreferenceTimeZone string `db:"preference_time_zone"`
PreferredLocale string `db:"preferred_locale"`
Superuser bool `db:"superuser"`
}

Expand All @@ -695,6 +696,7 @@ type InsertUserOptions struct {
QuotaSingleuserOrgs int
QuotaTrialOrgs int
Superuser bool
PreferredLocale string
}

// UpdateUserOptions defines options for updating an existing user
Expand All @@ -708,6 +710,7 @@ type UpdateUserOptions struct {
QuotaSingleuserOrgs int
QuotaTrialOrgs int
PreferenceTimeZone string
PreferredLocale string
}

// Service represents a service account.
Expand Down
1 change: 1 addition & 0 deletions admin/database/postgres/migrations/0096.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN preferred_locale TEXT NOT NULL DEFAULT '';
7 changes: 4 additions & 3 deletions admin/database/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,7 @@ func (c *connection) InsertUser(ctx context.Context, opts *database.InsertUserOp
}

res := &database.User{}
err := c.getDB(ctx).QueryRowxContext(ctx, "INSERT INTO users (email, display_name, photo_url, quota_trial_orgs, quota_singleuser_orgs, superuser) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *", opts.Email, opts.DisplayName, opts.PhotoURL, opts.QuotaTrialOrgs, opts.QuotaSingleuserOrgs, opts.Superuser).StructScan(res)
err := c.getDB(ctx).QueryRowxContext(ctx, "INSERT INTO users (email, display_name, photo_url, quota_trial_orgs, quota_singleuser_orgs, superuser, preferred_locale) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *", opts.Email, opts.DisplayName, opts.PhotoURL, opts.QuotaTrialOrgs, opts.QuotaSingleuserOrgs, opts.Superuser, opts.PreferredLocale).StructScan(res)
if err != nil {
return nil, parseErr("user", err)
}
Expand All @@ -935,7 +935,7 @@ func (c *connection) UpdateUser(ctx context.Context, id string, opts *database.U
}

res := &database.User{}
err := c.getDB(ctx).QueryRowxContext(ctx, "UPDATE users SET display_name=$2, photo_url=$3, github_username=$4, github_token=$5, github_token_expires_on=$6, github_refresh_token=$7, quota_singleuser_orgs=$8, quota_trial_orgs=$9, preference_time_zone=$10, updated_on=now() WHERE id=$1 RETURNING *",
err := c.getDB(ctx).QueryRowxContext(ctx, "UPDATE users SET display_name=$2, photo_url=$3, github_username=$4, github_token=$5, github_token_expires_on=$6, github_refresh_token=$7, quota_singleuser_orgs=$8, quota_trial_orgs=$9, preference_time_zone=$10, preferred_locale=$11, updated_on=now() WHERE id=$1 RETURNING *",
id,
opts.DisplayName,
opts.PhotoURL,
Expand All @@ -945,7 +945,8 @@ func (c *connection) UpdateUser(ctx context.Context, id string, opts *database.U
opts.GithubRefreshToken,
opts.QuotaSingleuserOrgs,
opts.QuotaTrialOrgs,
opts.PreferenceTimeZone).StructScan(res)
opts.PreferenceTimeZone,
opts.PreferredLocale).StructScan(res)
if err != nil {
return nil, parseErr("user", err)
}
Expand Down
3 changes: 2 additions & 1 deletion admin/server/auth/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ func (a *Authenticator) authLoginCallback(w http.ResponseWriter, r *http.Request
http.Error(w, "claim 'picture' not found", http.StatusInternalServerError)
return
}
locale, _ := profile["locale"].(string)

// Get redirect destination
redirect, ok := sess.Values[cookieFieldRedirect].(string)
Expand All @@ -324,7 +325,7 @@ func (a *Authenticator) authLoginCallback(w http.ResponseWriter, r *http.Request
}

// Create (or update) user in our DB
user, err := a.admin.CreateOrUpdateUser(r.Context(), email, name, photoURL)
user, err := a.admin.CreateOrUpdateUser(r.Context(), email, name, photoURL, locale)
if err != nil {
http.Error(w, fmt.Sprintf("failed to update user: %s", err), http.StatusInternalServerError)
return
Expand Down
20 changes: 18 additions & 2 deletions admin/server/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
adminv1 "github.com/rilldata/rill/proto/gen/rill/admin/v1"
"github.com/rilldata/rill/runtime/pkg/observability"
"go.opentelemetry.io/otel/attribute"
"golang.org/x/text/language"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb"
Expand Down Expand Up @@ -123,7 +124,8 @@ func (s *Server) GetCurrentUser(ctx context.Context, req *adminv1.GetCurrentUser
return &adminv1.GetCurrentUserResponse{
User: s.userToPB(u, true),
Preferences: &adminv1.UserPreferences{
TimeZone: &u.PreferenceTimeZone,
TimeZone: &u.PreferenceTimeZone,
PreferredLocale: &u.PreferredLocale,
},
}, nil
}
Expand All @@ -136,6 +138,10 @@ func (s *Server) UpdateUserPreferences(ctx context.Context, req *adminv1.UpdateU
return nil, status.Error(codes.Unauthenticated, "not authenticated as a user")
}

if req == nil || req.Preferences == nil {
return nil, status.Error(codes.InvalidArgument, "preferences are required")
}

if req.Preferences.TimeZone != nil {
_, err := time.LoadLocation(*req.Preferences.TimeZone)
if err != nil {
Expand All @@ -145,6 +151,14 @@ func (s *Server) UpdateUserPreferences(ctx context.Context, req *adminv1.UpdateU
observability.AddRequestAttributes(ctx, attribute.String("preferences_time_zone", *req.Preferences.TimeZone))
}

if req.Preferences.PreferredLocale != nil && *req.Preferences.PreferredLocale != "" {
_, err := language.Parse(*req.Preferences.PreferredLocale)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid language tag: %s", *req.Preferences.PreferredLocale)
}
observability.AddRequestAttributes(ctx, attribute.String("preferences_preferred_locale", *req.Preferences.PreferredLocale))
}

// Owner is a user
user, err := s.admin.DB.FindUser(ctx, claims.OwnerID())
if err != nil {
Expand All @@ -162,14 +176,16 @@ func (s *Server) UpdateUserPreferences(ctx context.Context, req *adminv1.UpdateU
QuotaSingleuserOrgs: user.QuotaSingleuserOrgs,
QuotaTrialOrgs: user.QuotaTrialOrgs,
PreferenceTimeZone: valOrDefault(req.Preferences.TimeZone, user.PreferenceTimeZone),
PreferredLocale: valOrDefault(req.Preferences.PreferredLocale, user.PreferredLocale),
})
if err != nil {
return nil, err
}

return &adminv1.UpdateUserPreferencesResponse{
Preferences: &adminv1.UserPreferences{
TimeZone: &updatedUser.PreferenceTimeZone,
TimeZone: &updatedUser.PreferenceTimeZone,
PreferredLocale: &updatedUser.PreferredLocale,
},
}, nil
}
Expand Down
36 changes: 36 additions & 0 deletions admin/server/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,40 @@ func TestUser(t *testing.T) {
}

})

t.Run("Preference preferred_locale", func(t *testing.T) {
_, c1 := fix.NewUser(t)
resp, err := c1.UpdateUserPreferences(ctx, &adminv1.UpdateUserPreferencesRequest{
Preferences: &adminv1.UserPreferences{
PreferredLocale: strPtr("es"),
},
})
require.NoError(t, err)
require.Equal(t, "es", *resp.Preferences.PreferredLocale)
cur, err := c1.GetCurrentUser(ctx, &adminv1.GetCurrentUserRequest{})
require.NoError(t, err)
require.Equal(t, "es", *cur.Preferences.PreferredLocale)
})

t.Run("Preference preferred_locale invalid", func(t *testing.T) {
_, c1 := fix.NewUser(t)
_, err := c1.UpdateUserPreferences(ctx, &adminv1.UpdateUserPreferencesRequest{
Preferences: &adminv1.UserPreferences{
PreferredLocale: strPtr("a"),
},
})
require.Error(t, err)
require.Equal(t, codes.InvalidArgument, status.Code(err))
})

t.Run("Nil preferences", func(t *testing.T) {
_, c1 := fix.NewUser(t)
_, err := c1.UpdateUserPreferences(ctx, &adminv1.UpdateUserPreferencesRequest{})
require.Error(t, err)
require.Equal(t, codes.InvalidArgument, status.Code(err))
})
}

func strPtr(s string) *string {
return &s
}
2 changes: 1 addition & 1 deletion admin/testadmin/testadmin.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ func (f *Fixture) NewUserWithEmail(t *testing.T, emailAddr string) (*database.Us
ctx := t.Context()
name := fmt.Sprintf("Test %s", strings.Split(emailAddr, "@")[0])

u, err := f.Admin.CreateOrUpdateUser(ctx, emailAddr, name, "")
u, err := f.Admin.CreateOrUpdateUser(ctx, emailAddr, name, "", "")
require.NoError(t, err)

tkn, err := f.Admin.IssueUserAuthToken(ctx, u.ID, database.AuthClientIDRillWeb, "Test session", nil, nil, false)
Expand Down
15 changes: 14 additions & 1 deletion admin/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/rilldata/rill/admin/database"
"go.uber.org/zap"
"golang.org/x/text/language"
)

// InsertOrganizationMemberUser inserts a user as a member of an organization.
Expand Down Expand Up @@ -128,16 +129,26 @@ func (s *Service) UpdateOrganizationMemberUserRole(ctx context.Context, orgID, u

// CreateOrUpdateUser creates or updates a user with the given email, name, and photo URL.
// If the user doesn't exist, it creates a new user and simultaneously adds them to any orgs and projects they have been invited to.
func (s *Service) CreateOrUpdateUser(ctx context.Context, email, name, photoURL string) (*database.User, error) {
func (s *Service) CreateOrUpdateUser(ctx context.Context, email, name, photoURL, locale string) (*database.User, error) {
// Validate email address
_, err := mail.ParseAddress(email)
if err != nil {
return nil, fmt.Errorf("invalid user email address %q", email)
}

if locale != "" {
if _, err := language.Parse(locale); err != nil {
locale = ""
}
}

// Update user if exists
user, err := s.DB.FindUserByEmail(ctx, email)
if err == nil {
lang := user.PreferredLocale
if lang == "" && locale != "" {
lang = locale
}
return s.DB.UpdateUser(ctx, user.ID, &database.UpdateUserOptions{
DisplayName: name,
PhotoURL: photoURL,
Expand All @@ -148,6 +159,7 @@ func (s *Service) CreateOrUpdateUser(ctx context.Context, email, name, photoURL
QuotaSingleuserOrgs: user.QuotaSingleuserOrgs,
QuotaTrialOrgs: user.QuotaTrialOrgs,
PreferenceTimeZone: user.PreferenceTimeZone,
PreferredLocale: lang,
})
} else if !errors.Is(err, database.ErrNotFound) {
return nil, err
Expand Down Expand Up @@ -183,6 +195,7 @@ func (s *Service) CreateOrUpdateUser(ctx context.Context, email, name, photoURL
QuotaSingleuserOrgs: deref(s.Biller.DefaultUserQuotas().SingleuserOrgs, -1),
QuotaTrialOrgs: deref(s.Biller.DefaultUserQuotas().TrialOrgs, -1),
Superuser: isFirstUser,
PreferredLocale: locale,
}

// Create user
Expand Down
Loading