diff --git a/frontend/src/common/session.js b/frontend/src/common/session.js index 71b7b3000..b1c9e94db 100644 --- a/frontend/src/common/session.js +++ b/frontend/src/common/session.js @@ -138,9 +138,9 @@ export default class Session { return ""; } - getFirstName() { + getGivenName() { if (this.isUser()) { - return this.user.FirstName; + return this.user.GivenName; } return ""; @@ -148,7 +148,7 @@ export default class Session { getFullName() { if (this.isUser()) { - return this.user.FirstName + " " + this.user.LastName; + return this.user.GivenName + " " + this.user.FamilyName; } return ""; @@ -159,7 +159,7 @@ export default class Session { } isAdmin() { - return this.user && this.user.hasId() && this.user.Admin; + return this.user && this.user.hasId() && this.user.RoleAdmin; } isAnonymous() { diff --git a/frontend/src/model/user.js b/frontend/src/model/user.js index 5708a642c..605e43b47 100644 --- a/frontend/src/model/user.js +++ b/frontend/src/model/user.js @@ -38,28 +38,81 @@ export class User extends RestModel { return { ID: 0, UID: "", + ParentUID: "", + UserUUID: "", UserName: "", - FirstName: "", - LastName: "", + UserLocale: "", + TimeZone: "", + PrimaryEmail: "", + BackupEmail: "", DisplayName: "", - Email: "", - Info: "", - Notes: "", - Active: false, - Confirmed: false, - Admin: false, - Guest: false, - Child: false, - Family: false, - Friend: false, - Artist: false, - Subject: false, + DisplayLocation: "", + DisplayBio: "", + NamePrefix: "", + GivenName: "", + FamilyName: "", + NameSuffix: "", + AvatarUID: "", + AvatarURL: "", + FeedURL: "", + FeedType: "", + FeedFollow: false, + BlogURL: "", + BlogType: "", + BlogFollow: false, + CompanyURL: "", + CompanyName: "", + CompanyPhone: "", + PrimaryPhone: "", + DepartmentName: "", + JobTitle: "", + AddressLat: 0.0, + AddressLng: 0.0, + AddressLine1: "", + AddressLine2: "", + AddressZip: "", + AddressCity: "", + AddressState: "", + AddressCountry: "", + TermsAccepted: false, + IsActive: false, + IsConfirmed: false, + IsPro: false, + IsSponsor: false, + IsContributor: false, + IsArtist: false, + IsSubject: false, + RoleAdmin: false, + RoleGuest: false, + RoleChild: false, + RoleFamily: false, + RoleFriend: false, CanEdit: false, + CanDelete: false, + CanIndex: false, + CanShare: false, CanComment: false, CanUpload: false, CanDownload: false, WebDAV: false, ApiToken: "", + AmazonID: "", + AppleID: "", + EyeEmID: "", + FacebookID: "", + FlickrID: "", + GitHubID: "", + GitLabID: "", + GoogleID: "", + InstagramID: "", + LinkedinID: "", + MastodonID: "", + NextcloudID: "", + TelegramID: "", + TwitterID: "", + WhatsAppID: "", + YouTubeID: "", + UserNotes: "", LoginAttempts: 0, LoginAt: "", CreatedAt: "", @@ -68,7 +121,7 @@ export class User extends RestModel { } getEntityName() { - return this.FirstName + " " + this.LastName; + return this.GivenName + " " + this.FamilyName; } getRegisterForm() { diff --git a/frontend/tests/unit/common/session_test.js b/frontend/tests/unit/common/session_test.js index 952328f84..23cdf32cb 100644 --- a/frontend/tests/unit/common/session_test.js +++ b/frontend/tests/unit/common/session_test.js @@ -185,12 +185,12 @@ describe('common/session', () => { const storage = window.localStorage; const session = new Session(storage, config); assert.isFalse(session.user.hasId()); - const values = {"user": {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Admin: true}}; + const values = {"user": {ID: 5, GivenName: "Max", FamilyName: "Last", Email: "test@test.com", RoleAdmin: true}}; session.setData(); - assert.equal(session.user.FirstName, ""); + assert.equal(session.user.GivenName, ""); session.setData(values); - assert.equal(session.user.FirstName, "Max"); - assert.equal(session.user.Admin, true); + assert.equal(session.user.GivenName, "Max"); + assert.equal(session.user.RoleAdmin, true); const result = session.getUser(); assert.equal(result.ID, 5); assert.equal(result.Email, "test@test.com"); @@ -201,11 +201,11 @@ describe('common/session', () => { it('should get user email', () => { const storage = window.localStorage; const session = new Session(storage, config); - const values = {"user": {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Admin: true}}; + const values = {"user": {ID: 5, GivenName: "Max", FamilyName: "Last", Email: "test@test.com", RoleAdmin: true}}; session.setData(values); const result = session.getEmail(); assert.equal(result, "test@test.com"); - const values2 = {"user": {FirstName: "Max", LastName: "Last", Email: "test@test.com", Admin: true}}; + const values2 = {"user": {GivenName: "Max", FamilyName: "Last", Email: "test@test.com", RoleAdmin: true}}; session.setData(values2); const result2 = session.getEmail(); assert.equal(result2, ""); @@ -215,13 +215,13 @@ describe('common/session', () => { it('should get user firstname', () => { const storage = window.localStorage; const session = new Session(storage, config); - const values = {"user": {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Admin: true}}; + const values = {"user": {ID: 5, GivenName: "Max", FamilyName: "Last", Email: "test@test.com", RoleAdmin: true}}; session.setData(values); - const result = session.getFirstName(); + const result = session.getGivenName(); assert.equal(result, "Max"); - const values2 = {"user": {FirstName: "Max", LastName: "Last", Email: "test@test.com", Admin: true}}; + const values2 = {"user": {GivenName: "Max", FamilyName: "Last", Email: "test@test.com", RoleAdmin: true}}; session.setData(values2); - const result2 = session.getFirstName(); + const result2 = session.getGivenName(); assert.equal(result2, ""); session.deleteData(); }); @@ -229,11 +229,11 @@ describe('common/session', () => { it('should get user full name', () => { const storage = window.localStorage; const session = new Session(storage, config); - const values = {"user": {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Admin: true}}; + const values = {"user": {ID: 5, GivenName: "Max", FamilyName: "Last", Email: "test@test.com", RoleAdmin: true}}; session.setData(values); const result = session.getFullName(); assert.equal(result, "Max Last"); - const values2 = {"user": {FirstName: "Max", LastName: "Last", Email: "test@test.com", Admin: true}}; + const values2 = {"user": {GivenName: "Max", FamilyName: "Last", Email: "test@test.com", RoleAdmin: true}}; session.setData(values2); const result2 = session.getFullName(); assert.equal(result2, ""); @@ -243,7 +243,7 @@ describe('common/session', () => { it('should test whether user is set', () => { const storage = window.localStorage; const session = new Session(storage, config); - const values = {"user": {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Admin: true}}; + const values = {"user": {ID: 5, GivenName: "Max", FamilyName: "Last", Email: "test@test.com", RoleAdmin: true}}; session.setData(values); const result = session.isUser(); assert.equal(result, true); @@ -253,7 +253,7 @@ describe('common/session', () => { it('should test whether user is admin', () => { const storage = window.localStorage; const session = new Session(storage, config); - const values = {"user": {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Admin: true}}; + const values = {"user": {ID: 5, GivenName: "Max", FamilyName: "Last", Email: "test@test.com", RoleAdmin: true}}; session.setData(values); const result = session.isAdmin(); assert.equal(result, true); @@ -263,7 +263,7 @@ describe('common/session', () => { it('should test whether user is anonymous', () => { const storage = window.localStorage; const session = new Session(storage, config); - const values = {"user": {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Admin: true}}; + const values = {"user": {ID: 5, GivenName: "Max", FamilyName: "Last", Email: "test@test.com", RoleAdmin: true}}; session.setData(values); const result = session.isAnonymous(); assert.equal(result, false); diff --git a/frontend/tests/unit/model/user_test.js b/frontend/tests/unit/model/user_test.js index 183905f06..a4aa5fb3b 100644 --- a/frontend/tests/unit/model/user_test.js +++ b/frontend/tests/unit/model/user_test.js @@ -9,14 +9,14 @@ describe("model/user", () => { const mock = new MockAdapter(Api); it("should get entity name", () => { - const values = {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"}; + const values = {ID: 5, GivenName: "Max", FamilyName: "Last", Email: "test@test.com", Role: "admin"}; const user = new User(values); const result = user.getEntityName(); assert.equal(result, "Max Last"); }); it("should get id", () => { - const values = {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"}; + const values = {ID: 5, GivenName: "Max", FamilyName: "Last", Email: "test@test.com", Role: "admin"}; const user = new User(values); const result = user.getId(); assert.equal(result, 5); @@ -34,7 +34,7 @@ describe("model/user", () => { it("should get register form", async() => { mock.onAny("users/52/register").reply(200, "registerForm"); - const values = {ID: 52, FirstName: "Max"}; + const values = {ID: 52, GivenName: "Max"}; const user = new User(values); const result = await user.getRegisterForm(); assert.equal(result.definition, "registerForm"); @@ -43,7 +43,7 @@ describe("model/user", () => { it("should get profile form", async() => { mock.onAny("users/53/profile").reply(200, "profileForm"); - const values = {ID: 53, FirstName: "Max"}; + const values = {ID: 53, GivenName: "Max"}; const user = new User(values); const result = await user.getProfileForm(); assert.equal(result.definition, "profileForm"); @@ -52,20 +52,20 @@ describe("model/user", () => { it("should get change password", async() => { mock.onPut("users/54/password").reply(200, {password: "old", new_password: "new"}); - const values = {ID: 54, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"}; + const values = {ID: 54, GivenName: "Max", FamilyName: "Last", Email: "test@test.com", Role: "admin"}; const user = new User(values); const result = await user.changePassword("old", "new"); assert.equal(result.new_password, "new"); }); it("should save profile", async() => { - mock.onPost("users/55/profile").reply(200, {FirstName: "MaxNew", LastName: "LastNew"}); - const values = {ID: 55, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"}; + mock.onPost("users/55/profile").reply(200, {GivenName: "MaxNew", FamilyName: "LastNew"}); + const values = {ID: 55, GivenName: "Max", FamilyName: "Last", Email: "test@test.com", Role: "admin"}; const user = new User(values); - assert.equal(user.FirstName, "Max"); - assert.equal(user.LastName, "Last"); + assert.equal(user.GivenName, "Max"); + assert.equal(user.FamilyName, "Last"); await user.saveProfile(); - assert.equal(user.FirstName, "MaxNew"); - assert.equal(user.LastName, "LastNew"); + assert.equal(user.GivenName, "MaxNew"); + assert.equal(user.FamilyName, "LastNew"); }); }); diff --git a/internal/config/client.go b/internal/config/client.go index 8f7e2566e..32f52db2c 100644 --- a/internal/config/client.go +++ b/internal/config/client.go @@ -10,7 +10,7 @@ import ( "github.com/photoprism/photoprism/pkg/txt" ) -// ClientConfig contains HTTP client / Web UI config values +// ClientConfig represents HTTP client / Web UI config values. type ClientConfig struct { Name string `json:"name"` Version string `json:"version"` @@ -26,6 +26,9 @@ type ClientConfig struct { ReadOnly bool `json:"readonly"` UploadNSFW bool `json:"uploadNSFW"` Public bool `json:"public"` + Pro bool `json:"pro"` + Sponsor bool `json:"sponsor"` + Contributor bool `json:"contributor"` Experimental bool `json:"experimental"` DisableSettings bool `json:"disableSettings"` AlbumCategories []string `json:"albumCategories"` @@ -34,6 +37,8 @@ type ClientConfig struct { Lenses []entity.Lens `json:"lenses"` Countries []entity.Country `json:"countries"` Thumbs []Thumb `json:"thumbs"` + ApiKey string `json:"apiKey"` + MapsKey string `json:"mapsKey"` DownloadToken string `json:"downloadToken"` PreviewToken string `json:"previewToken"` JSHash string `json:"jsHash"` diff --git a/internal/config/config.go b/internal/config/config.go index 4daea74fb..e4aede4a2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "context" + "fmt" "runtime" "strings" "sync" @@ -11,6 +12,7 @@ import ( _ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/sqlite" "github.com/photoprism/photoprism/internal/event" + "github.com/photoprism/photoprism/internal/maps/places" "github.com/photoprism/photoprism/internal/mutex" "github.com/photoprism/photoprism/internal/thumb" "github.com/photoprism/photoprism/pkg/rnd" @@ -23,11 +25,12 @@ var once sync.Once // Config holds database, cache and all parameters of photoprism type Config struct { - once sync.Once - db *gorm.DB - params *Params - settings *Settings - token string + once sync.Once + db *gorm.DB + params *Params + settings *Settings + credentials *Credentials + token string } func init() { @@ -67,6 +70,7 @@ func NewConfig(ctx *cli.Context) *Config { } c.initSettings() + c.initCredentials() return c } @@ -79,12 +83,14 @@ func (c *Config) Propagate() { thumb.SizeUncached = c.ThumbSizeUncached() thumb.Filter = c.ThumbFilter() thumb.JpegQuality = c.JpegQuality() + places.UserAgent = c.UserAgent() c.Settings().Propagate() + c.Credentials().Propagate() } // Init initialises the database connection and dependencies. -func (c *Config) Init(ctx context.Context) error { +func (c *Config) Init(_ context.Context) error { c.Propagate() return c.connectDb() } @@ -99,6 +105,11 @@ func (c *Config) Version() string { return c.params.Version } +// UserAgent returns a HTTP user agent string based on app name & version. +func (c *Config) UserAgent() string { + return fmt.Sprintf("%s/%s", c.Name(), c.Version()) +} + // Copyright returns the application copyright. func (c *Config) Copyright() string { return c.params.Copyright diff --git a/internal/config/credentials.go b/internal/config/credentials.go new file mode 100644 index 000000000..3f6f0a2a5 --- /dev/null +++ b/internal/config/credentials.go @@ -0,0 +1,107 @@ +package config + +import ( + "crypto/sha1" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/photoprism/photoprism/internal/maps/places" + "github.com/photoprism/photoprism/pkg/fs" + "github.com/photoprism/photoprism/pkg/txt" + "gopkg.in/yaml.v2" +) + +// Credentials represents api credentials for hosted services like maps & places. +type Credentials struct { + Key string `json:"key" yaml:"key"` + Secret string `json:"secret" yaml:"secret"` + Session string `json:"session" yaml:"session"` +} + +// NewCredentials creates a new Credentials instance. +func NewCredentials() *Credentials { + return &Credentials{ + Key: "", + Secret: "", + Session: "", + } +} + +// Propagate updates api credentials in other packages. +func (a *Credentials) Propagate() { + places.Key = a.Key +} + +// Sanitize verifies and sanitizes api credentials; +func (a *Credentials) Sanitize() { + a.Key = strings.ToLower(a.Key) + + if a.Secret != "" { + if a.Key != fmt.Sprintf("%x", sha1.Sum([]byte(a.Secret))) { + a.Secret = "" + a.Session = "" + } + } +} + +// Load api credentials from a file. +func (a *Credentials) Load(fileName string) error { + if !fs.FileExists(fileName) { + return fmt.Errorf("credentials file not found: %s", txt.Quote(fileName)) + } + + yamlConfig, err := ioutil.ReadFile(fileName) + + if err != nil { + return err + } + + if err := yaml.Unmarshal(yamlConfig, a); err != nil { + return err + } + + a.Sanitize() + a.Propagate() + + return nil +} + +// Save api credentials to a file. +func (a *Credentials) Save(fileName string) error { + a.Sanitize() + + data, err := yaml.Marshal(a) + + if err != nil { + return err + } + + a.Propagate() + + if err := ioutil.WriteFile(fileName, data, os.ModePerm); err != nil { + return err + } + + a.Propagate() + + return nil +} + +// initCredentials initializes the api credentials. +func (c *Config) initCredentials() { + c.credentials = NewCredentials() + p := c.CredentialsFile() + + if err := c.credentials.Load(p); err != nil { + log.Traceln(err) + } + + c.credentials.Propagate() +} + +// Credentials returns the api key instance. +func (c *Config) Credentials() *Credentials { + return c.credentials +} diff --git a/internal/config/credentials_test.go b/internal/config/credentials_test.go new file mode 100644 index 000000000..f70fc0a6f --- /dev/null +++ b/internal/config/credentials_test.go @@ -0,0 +1,84 @@ +package config + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewCredentials(t *testing.T) { + c := NewCredentials() + + assert.IsType(t, &Credentials{}, c) +} + +func TestCredentials_Load(t *testing.T) { + t.Run("existing filename", func(t *testing.T) { + c := NewCredentials() + + if err := c.Load("testdata/credentials.yml"); err != nil { + t.Fatal(err) + } + + assert.Equal(t, "f60f5b25d59c397989e3cd374f81cdd7710a4fca", c.Key) + assert.Equal(t, "photoprism", c.Secret) + assert.Equal(t, "Zm9vYmFy", c.Session) + }) + t.Run("not existing filename", func(t *testing.T) { + c := NewCredentials() + + if err := c.Load("testdata/credentials_xxx.yml"); err == nil { + t.Fatal("file should not exist") + } + + assert.Equal(t, "", c.Key) + assert.Equal(t, "", c.Secret) + assert.Equal(t, "", c.Session) + }) +} +func TestCredentials_Save(t *testing.T) { + t.Run("existing filename", func(t *testing.T) { + assert.FileExists(t, "testdata/credentials.yml") + + c := NewCredentials() + + if err := c.Load("testdata/credentials.yml"); err != nil { + t.Fatal(err) + } + + assert.Equal(t, "f60f5b25d59c397989e3cd374f81cdd7710a4fca", c.Key) + assert.Equal(t, "photoprism", c.Secret) + assert.Equal(t, "Zm9vYmFy", c.Session) + + if err := c.Save("testdata/credentials.yml"); err != nil { + t.Fatal(err) + } + + assert.FileExists(t, "testdata/credentials.yml") + }) + t.Run("not existing filename", func(t *testing.T) { + c := NewCredentials() + c.Key = "F60F5B25D59C397989E3CD374F81CDD7710A4FCA" + c.Secret = "foo" + c.Session = "bar" + + assert.Equal(t, "F60F5B25D59C397989E3CD374F81CDD7710A4FCA", c.Key) + assert.Equal(t, "foo", c.Secret) + assert.Equal(t, "bar", c.Session) + + if err := c.Save("testdata/credentials_new.yml"); err != nil { + t.Fatal(err) + } + + assert.Equal(t, "f60f5b25d59c397989e3cd374f81cdd7710a4fca", c.Key) + assert.Equal(t, "", c.Secret) + assert.Equal(t, "", c.Session) + + assert.FileExists(t, "testdata/credentials_new.yml") + + if err := os.Remove("testdata/credentials_new.yml"); err != nil { + t.Fatal(err) + } + }) +} diff --git a/internal/config/filenames.go b/internal/config/filenames.go index f77ca5058..630006ebd 100644 --- a/internal/config/filenames.go +++ b/internal/config/filenames.go @@ -137,6 +137,11 @@ func (c *Config) ConfigFile() string { return c.params.ConfigFile } +// CredentialsFile returns the api credentials file name for hosted services like maps & places. +func (c *Config) CredentialsFile() string { + return filepath.Join(c.SettingsPath(), "credentials.yml") +} + // SettingsFile returns the user settings file name. func (c *Config) SettingsFile() string { return filepath.Join(c.SettingsPath(), "settings.yml") @@ -194,7 +199,7 @@ func (c *Config) ExifToolBin() string { return findExecutable(c.params.ExifToolBin, "exiftool") } -// SidecarJson returns true if metadata should be synced with json sidecar files as used by exiftool. +// Automatically create JSON sidecar files using Exiftool. func (c *Config) SidecarJson() bool { if !c.SidecarWritable() || c.ExifToolBin() == "" { return false @@ -203,7 +208,7 @@ func (c *Config) SidecarJson() bool { return c.params.SidecarJson } -// SidecarYaml returns true if metadata should be synced with PhotoPrism YAML sidecar files. +// Automatically backup metadata to YAML sidecar files. func (c *Config) SidecarYaml() bool { if !c.SidecarWritable() { return false @@ -212,7 +217,7 @@ func (c *Config) SidecarYaml() bool { return c.params.SidecarYaml } -// SidecarPath returns the storage path for automatically created sidecar files. +// SidecarPath returns the storage path for generated sidecar files (relative or absolute). func (c *Config) SidecarPath() string { if c.params.SidecarPath == "" { c.params.SidecarPath = filepath.Join(c.StoragePath(), "sidecar") diff --git a/internal/config/settings.go b/internal/config/settings.go index 023a46ac7..8e285181f 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -16,15 +16,18 @@ func (c *Config) SettingsHidden() bool { return c.params.SettingsHidden } +// TemplateSettings represents HTML template settings for the Web UI. type TemplateSettings struct { Default string `json:"default" yaml:"default"` } +// MapsSettings represents maps settings (for places). type MapsSettings struct { Animate int `json:"animate" yaml:"animate"` Style string `json:"style" yaml:"style"` } +// IndexSettings represents indexing settings. type IndexSettings struct { Path string `json:"path" yaml:"path"` Convert bool `json:"convert" yaml:"convert"` @@ -32,11 +35,13 @@ type IndexSettings struct { Sequences bool `json:"sequences" yaml:"sequences"` } +// ImportSettings represents import settings. type ImportSettings struct { Path string `json:"path" yaml:"path"` Move bool `json:"move" yaml:"move"` } +// FeatureSettings represents feature flags, mainly for the Web UI. type FeatureSettings struct { Upload bool `json:"upload" yaml:"upload"` Download bool `json:"download" yaml:"download"` @@ -54,7 +59,7 @@ type FeatureSettings struct { Logs bool `json:"logs" yaml:"logs"` } -// Settings contains Web UI settings +// Settings represents user settings for Web UI, indexing, and import. type Settings struct { Theme string `json:"theme" yaml:"theme"` Language string `json:"language" yaml:"language"` @@ -65,7 +70,7 @@ type Settings struct { Index IndexSettings `json:"index" yaml:"index"` } -// NewSettings returns a empty Settings +// NewSettings creates a new Settings instance. func NewSettings() *Settings { return &Settings{ Theme: "default", @@ -111,7 +116,7 @@ func (s Settings) Propagate() { i18n.SetLocale(s.Language) } -// Load uses a yaml config file to initiate the configuration entity. +// Load user settings from file. func (s *Settings) Load(fileName string) error { if !fs.FileExists(fileName) { return fmt.Errorf("settings file not found: %s", txt.Quote(fileName)) @@ -132,7 +137,7 @@ func (s *Settings) Load(fileName string) error { return nil } -// Save uses a yaml config file to initiate the configuration entity. +// Save user settings to a file. func (s *Settings) Save(fileName string) error { data, err := yaml.Marshal(s) diff --git a/internal/config/settings_test.go b/internal/config/settings_test.go index 966a37105..ca6c27bee 100644 --- a/internal/config/settings_test.go +++ b/internal/config/settings_test.go @@ -9,6 +9,7 @@ import ( func TestNewSettings(t *testing.T) { c := NewSettings() + assert.IsType(t, new(Settings), c) } @@ -16,9 +17,9 @@ func TestSettings_Load(t *testing.T) { t.Run("existing filename", func(t *testing.T) { c := NewSettings() - err := c.Load("testdata/config.yml") - - assert.Nil(t, err) + if err := c.Load("testdata/config.yml"); err != nil { + t.Fatal(err) + } assert.Equal(t, "lavendel", c.Theme) assert.Equal(t, "english", c.Language) @@ -43,9 +44,9 @@ func TestSettings_Save(t *testing.T) { assert.Equal(t, "lavendel", c.Theme) assert.Equal(t, "german", c.Language) - err := c.Save("testdata/configEmpty.yml") - - assert.Nil(t, err) + if err := c.Save("testdata/configEmpty.yml"); err != nil { + t.Fatal(err) + } }) t.Run("not existing filename", func(t *testing.T) { c := NewSettings() @@ -55,9 +56,7 @@ func TestSettings_Save(t *testing.T) { assert.Equal(t, "lavendel", c.Theme) assert.Equal(t, "german", c.Language) - err := c.Save("testdata/configEmpty123.yml") - - if err != nil { + if err := c.Save("testdata/configEmpty123.yml"); err != nil { t.Fatal(err) } diff --git a/internal/config/test.go b/internal/config/test.go index 3baba479b..07a8ef921 100644 --- a/internal/config/test.go +++ b/internal/config/test.go @@ -130,6 +130,7 @@ func NewTestConfig() *Config { } c.initSettings() + c.initCredentials() if err := c.Init(context.Background()); err != nil { log.Fatalf("config: %s", err.Error()) @@ -150,6 +151,7 @@ func NewTestErrorConfig() *Config { c := &Config{params: NewTestParamsError()} c.initSettings() + c.initCredentials() if err := c.Init(context.Background()); err != nil { log.Fatalf("config: %s", err.Error()) diff --git a/internal/config/testdata/credentials.yml b/internal/config/testdata/credentials.yml new file mode 100644 index 000000000..eea64abef --- /dev/null +++ b/internal/config/testdata/credentials.yml @@ -0,0 +1,3 @@ +key: f60f5b25d59c397989e3cd374f81cdd7710a4fca +secret: photoprism +session: Zm9vYmFy diff --git a/internal/entity/person.go b/internal/entity/person.go index 711140f9c..b24fffbdd 100644 --- a/internal/entity/person.go +++ b/internal/entity/person.go @@ -14,72 +14,138 @@ type People []Person // Person represents a real person that can also be a user if a password is set. type Person struct { - ID int `gorm:"primary_key" json:"ID" yaml:"-"` - PersonUID string `gorm:"type:varbinary(42);unique_index;" json:"UID" yaml:"UID"` - UserName string `gorm:"type:varchar(32);" json:"UserName" yaml:"UserName,omitempty"` - FirstName string `gorm:"type:varchar(32);" json:"FirstName" yaml:"FirstName,omitempty"` - LastName string `gorm:"type:varchar(32);" json:"LastName" yaml:"LastName,omitempty"` - DisplayName string `gorm:"type:varchar(64);" json:"DisplayName" yaml:"DisplayName,omitempty"` - UserEmail string `gorm:"type:varchar(255);" json:"Email" yaml:"Email,omitempty"` - UserInfo string `gorm:"type:text;" json:"Info" yaml:"Info,omitempty"` - UserPath string `json:"UserPath" yaml:"UserPath,omitempty"` - UserActive bool `json:"Active" yaml:"Active,omitempty"` - UserConfirmed bool `json:"Confirmed" yaml:"Confirmed,omitempty"` - RoleAdmin bool `json:"Admin" yaml:"Admin,omitempty"` - RoleGuest bool `json:"Guest" yaml:"Guest,omitempty"` - RoleChild bool `json:"Child" yaml:"Child,omitempty"` - RoleFamily bool `json:"Family" yaml:"Family,omitempty"` - RoleFriend bool `json:"Friend" yaml:"Friend,omitempty"` - IsArtist bool `json:"Artist" yaml:"Artist,omitempty"` - IsSubject bool `json:"Subject" yaml:"Subject,omitempty"` - CanEdit bool `json:"CanEdit" yaml:"CanEdit,omitempty"` - CanComment bool `json:"CanComment" yaml:"CanComment,omitempty"` - CanUpload bool `json:"CanUpload" yaml:"CanUpload,omitempty"` - CanDownload bool `json:"CanDownload" yaml:"CanDownload,omitempty"` - WebDAV bool `gorm:"column:webdav" json:"WebDAV" yaml:"WebDAV,omitempty"` - ApiToken string `json:"ApiToken" yaml:"ApiToken,omitempty"` - BirthYear int `json:"BirthYear" yaml:"BirthYear,omitempty"` - BirthMonth int `json:"BirthMonth" yaml:"BirthMonth,omitempty"` - BirthDay int `json:"BirthDay" yaml:"BirthDay,omitempty"` - LoginAttempts int `json:"-" yaml:"-,omitempty"` - LoginAt *time.Time `json:"-" yaml:"-"` - CreatedAt time.Time `json:"CreatedAt" yaml:"-"` - UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"` - DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"` + ID int `gorm:"primary_key" json:"ID" yaml:"-"` + PersonUID string `gorm:"type:varbinary(42);unique_index;" json:"UID" yaml:"UID"` + ParentUID string `gorm:"type:varbinary(42);" json:"ParentUID" yaml:"ParentUID,omitempty"` + UserUUID string `gorm:"type:varbinary(42);index;" json:"UserUUID" yaml:"UserUUID,omitempty"` + UserName string `gorm:"size:64;" json:"UserName" yaml:"UserName,omitempty"` + UserLocale string `gorm:"size:64;" json:"UserLocale" yaml:"UserLocale,omitempty"` + TimeZone string `gorm:"size:255;" json:"TimeZone" yaml:"TimeZone,omitempty"` + PrimaryEmail string `gorm:"size:255;index;" json:"PrimaryEmail" yaml:"PrimaryEmail,omitempty"` + BackupEmail string `gorm:"size:255;" json:"BackupEmail" yaml:"BackupEmail,omitempty"` + DisplayName string `gorm:"size:255;" json:"DisplayName" yaml:"DisplayName,omitempty"` + DisplayLocation string `gorm:"size:255;" json:"DisplayLocation" yaml:"DisplayLocation,omitempty"` + DisplayBio string `gorm:"type:text;" json:"DisplayBio" yaml:"DisplayBio,omitempty"` + NamePrefix string `gorm:"size:64;" json:"NamePrefix" yaml:"NamePrefix,omitempty"` + GivenName string `gorm:"size:128;" json:"GivenName" yaml:"GivenName,omitempty"` + FamilyName string `gorm:"size:128;" json:"FamilyName" yaml:"FamilyName,omitempty"` + NameSuffix string `gorm:"size:64;" json:"NameSuffix" yaml:"NameSuffix,omitempty"` + AvatarUID string `gorm:"type:varbinary(42);" json:"AvatarUID" yaml:"AvatarUID,omitempty"` + AvatarURL string `gorm:"size:255;" json:"AvatarURL" yaml:"AvatarURL,omitempty"` + FeedURL string `gorm:"size:255;" json:"FeedURL" yaml:"FeedURL,omitempty"` + FeedType string `gorm:"size:64" json:"FeedType" yaml:"FeedType,omitempty"` + FeedFollow bool `json:"FeedFollow" yaml:"FeedFollow,omitempty"` + BlogURL string `gorm:"size:255;" json:"BlogURL" yaml:"BlogURL,omitempty"` + BlogType string `gorm:"size:64;" json:"BlogType" yaml:"BlogType,omitempty"` + BlogFollow bool `json:"BlogFollow" yaml:"BlogFollow,omitempty"` + CompanyURL string `gorm:"size:255;" json:"CompanyURL" yaml:"CompanyURL,omitempty"` + CompanyName string `gorm:"size:128;" json:"CompanyName" yaml:"CompanyName,omitempty"` + CompanyPhone string `gorm:"size:255;" json:"CompanyPhone" yaml:"CompanyPhone,omitempty"` + PrimaryPhone string `gorm:"size:255;" json:"PrimaryPhone" yaml:"PrimaryPhone,omitempty"` + DepartmentName string `gorm:"size:255;" json:"DepartmentName" yaml:"DepartmentName,omitempty"` + JobTitle string `gorm:"size:255;" json:"JobTitle" yaml:"JobTitle,omitempty"` + AddressLat float32 `gorm:"type:FLOAT;index;" json:"AddressLat" yaml:"AddressLat,omitempty"` + AddressLng float32 `gorm:"type:FLOAT;index;" json:"AddressLng" yaml:"AddressLng,omitempty"` + AddressLine1 string `gorm:"size:255;" json:"AddressLine1" yaml:"AddressLine1,omitempty"` + AddressLine2 string `gorm:"size:255;" json:"AddressLine2" yaml:"AddressLine2,omitempty"` + AddressZip string `gorm:"size:255;" json:"AddressZip" yaml:"AddressZip,omitempty"` + AddressCity string `gorm:"size:255;" json:"AddressCity" yaml:"AddressCity,omitempty"` + AddressState string `gorm:"size:255;" json:"AddressState" yaml:"AddressState,omitempty"` + AddressCountry string `gorm:"type:varbinary(2);default:'zz'" json:"AddressCountry" yaml:"AddressCountry,omitempty"` + BirthYear int `json:"BirthYear" yaml:"BirthYear,omitempty"` + BirthMonth int `json:"BirthMonth" yaml:"BirthMonth,omitempty"` + BirthDay int `json:"BirthDay" yaml:"BirthDay,omitempty"` + TermsAccepted bool `json:"TermsAccepted" yaml:"TermsAccepted,omitempty"` + IsActive bool `json:"IsActive" yaml:"IsActive,omitempty"` + IsConfirmed bool `json:"IsConfirmed" yaml:"IsConfirmed,omitempty"` + IsPro bool `json:"IsPro" yaml:"IsPro,omitempty"` + IsSponsor bool `json:"IsSponsor" yaml:"IsSponsor,omitempty"` + IsContributor bool `json:"IsContributor" yaml:"IsContributor,omitempty"` + IsArtist bool `json:"IsArtist" yaml:"IsArtist,omitempty"` + IsSubject bool `json:"IsSubject" yaml:"IsSubject,omitempty"` + RoleAdmin bool `json:"RoleAdmin" yaml:"RoleAdmin,omitempty"` + RoleGuest bool `json:"RoleGuest" yaml:"RoleGuest,omitempty"` + RoleChild bool `json:"RoleChild" yaml:"RoleChild,omitempty"` + RoleFamily bool `json:"RoleFamily" yaml:"RoleFamily,omitempty"` + RoleFriend bool `json:"RoleFriend" yaml:"RoleFriend,omitempty"` + CanEdit bool `json:"CanEdit" yaml:"CanEdit,omitempty"` + CanDelete bool `json:"CanDelete" yaml:"CanDelete,omitempty"` + CanIndex bool `json:"CanIndex" yaml:"CanIndex,omitempty"` + CanShare bool `json:"CanShare" yaml:"CanShare,omitempty"` + CanComment bool `json:"CanComment" yaml:"CanComment,omitempty"` + CanUpload bool `json:"CanUpload" yaml:"CanUpload,omitempty"` + CanDownload bool `json:"CanDownload" yaml:"CanDownload,omitempty"` + HideLabels bool `json:"HideLabels" yaml:"HideLabels,omitempty"` + HidePlaces bool `json:"HidePlaces" yaml:"HidePlaces,omitempty"` + HidePeople bool `json:"HidePeople" yaml:"HidePeople,omitempty"` + HidePrivate bool `json:"HidePrivate" yaml:"HidePrivate,omitempty"` + HideLibrary bool `json:"HideLibrary" yaml:"HideLibrary,omitempty"` + HideSettings bool `json:"HideSettings" yaml:"HideSettings,omitempty"` + WebDAV bool `gorm:"column:webdav" json:"WebDAV" yaml:"WebDAV,omitempty"` + StoragePath string `gorm:"column:storage_path;size:255;" json:"StoragePath" yaml:"StoragePath,omitempty"` + ApiToken string `gorm:"column:api_token;size:255;" json:"ApiToken" yaml:"ApiToken,omitempty"` + ApiSecret string `gorm:"column:api_secret;size:255;" json:"-" yaml:"-"` + AmazonID string `gorm:"column:amazon_id;size:255;" json:"AmazonID" yaml:"AmazonID,omitempty"` + AppleID string `gorm:"column:apple_id;size:255;" json:"AppleID" yaml:"AppleID,omitempty"` + EyeEmID string `gorm:"column:eyeem_id;size:255;" json:"EyeEmID" yaml:"EyeEmID,omitempty"` + FacebookID string `gorm:"column:facebook_id;size:255;" json:"FacebookID" yaml:"FacebookID,omitempty"` + FlickrID string `gorm:"column:flickr_id;size:255;" json:"FlickrID" yaml:"FlickrID,omitempty"` + GitHubID string `gorm:"column:github_id;size:255;" json:"GitHubID" yaml:"GitHubID,omitempty"` + GitLabID string `gorm:"column:gitlab_id;size:255;" json:"GitLabID" yaml:"GitLabID,omitempty"` + GoogleID string `gorm:"column:google_id;size:255;" json:"GoogleID" yaml:"GoogleID,omitempty"` + InstagramID string `gorm:"column:instagram_id;size:255;" json:"InstagramID" yaml:"InstagramID,omitempty"` + LinkedinID string `gorm:"column:linkedin_id;size:255;" json:"LinkedinID" yaml:"LinkedinID,omitempty"` + MastodonID string `gorm:"column:mastodon_id;size:255;" json:"MastodonID" yaml:"MastodonID,omitempty"` + NextcloudID string `gorm:"column:nextcloud_id;size:255;" json:"NextcloudID" yaml:"NextcloudID,omitempty"` + TelegramID string `gorm:"column:telegram_id;size:255;" json:"TelegramID" yaml:"TelegramID,omitempty"` + TwitterID string `gorm:"column:twitter_id;size:255;" json:"TwitterID" yaml:"TwitterID,omitempty"` + WhatsAppID string `gorm:"column:whatsapp_id;size:255;" json:"WhatsAppID" yaml:"WhatsAppID,omitempty"` + YouTubeID string `gorm:"column:youtube_id;size:255;" json:"YouTubeID" yaml:"YouTubeID,omitempty"` + UserNotes string `gorm:"type:text;" json:"UserNotes" yaml:"UserNotes,omitempty"` + LoginAttempts int `json:"-" yaml:"-,omitempty"` + LoginAt *time.Time `json:"-" yaml:"-"` + CreatedAt time.Time `json:"CreatedAt" yaml:"-"` + UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"` + DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"` +} + +// TableName the database table name. +func (Person) TableName() string { + return "people" } // Default admin user. var Admin = Person{ - ID: 1, - UserName: "admin", - DisplayName: "Admin", - RoleAdmin: true, - UserActive: true, - UserConfirmed: true, + ID: 1, + UserName: "admin", + DisplayName: "Admin", + RoleAdmin: true, + IsActive: true, + IsConfirmed: true, } // Anonymous, public user without own account. var UnknownPerson = Person{ - ID: -1, - PersonUID: "u000000000000001", - UserName: "", - DisplayName: "Anonymous", - RoleAdmin: false, - RoleGuest: false, - UserActive: false, - UserConfirmed: false, + ID: -1, + PersonUID: "u000000000000001", + UserName: "", + DisplayName: "Anonymous", + RoleAdmin: false, + RoleGuest: false, + IsActive: false, + IsConfirmed: false, } // Guest user without own account for link sharing. var Guest = Person{ - ID: -2, - PersonUID: "u000000000000002", - UserName: "", - DisplayName: "Guest", - RoleAdmin: false, - RoleGuest: true, - UserActive: false, - UserConfirmed: false, + ID: -2, + PersonUID: "u000000000000002", + UserName: "", + DisplayName: "Guest", + RoleAdmin: false, + RoleGuest: true, + IsActive: false, + IsConfirmed: false, } // CreateDefaultUsers initializes the database with default user accounts. diff --git a/internal/maps/places/location.go b/internal/maps/places/location.go index 839948e2a..685dd4fac 100644 --- a/internal/maps/places/location.go +++ b/internal/maps/places/location.go @@ -24,8 +24,10 @@ type Location struct { const ApiName = "photoprism places" -var ReverseLookupURL = "https://places.photoprism.org/v1/location/%s" -var client = &http.Client{Timeout: 60 * time.Second} // TODO: Change timeout if needed +var Key = "" +var UserAgent = "PhotoPrism/DEVELOP" +var ReverseLookupURL = "https://places.photoprism.pro/v1/location/%s?key=%s" +var client = &http.Client{Timeout: 60 * time.Second} func NewLocation(id string, lat, lng float64, name, category string, place Place, cached bool) *Location { result := &Location{ @@ -63,7 +65,7 @@ func FindLocation(id string) (result Location, err error) { } } - url := fmt.Sprintf(ReverseLookupURL, id) + url := fmt.Sprintf(ReverseLookupURL, id, Key) log.Debugf("api: sending request to %s (%s)", url, ApiName) @@ -74,6 +76,8 @@ func FindLocation(id string) (result Location, err error) { return result, err } + req.Header.Set("User-Agent", UserAgent) + var r *http.Response for i := 0; i < 3; i++ {