mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Auth: Add "PHOTOPRISM_ADMIN_USER" option and refactor user table #98
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -131,24 +131,37 @@ export default class Session {
|
||||
|
||||
getEmail() {
|
||||
if (this.isUser()) {
|
||||
return this.user.PrimaryEmail;
|
||||
return this.user.Email;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
getNickName() {
|
||||
getUsername() {
|
||||
if (this.isUser()) {
|
||||
return this.user.NickName;
|
||||
if (this.user.Username) {
|
||||
return this.user.Username;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
getFullName() {
|
||||
getDisplayName() {
|
||||
if (this.isUser()) {
|
||||
if (this.user.DisplayName) {
|
||||
return this.user.DisplayName;
|
||||
}
|
||||
if (this.user.ArtistName) {
|
||||
return this.user.ArtistName;
|
||||
}
|
||||
if (this.user.FullName) {
|
||||
return this.user.FullName;
|
||||
}
|
||||
if (this.user.Username) {
|
||||
return this.user.Username;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
@@ -158,7 +171,7 @@ export default class Session {
|
||||
}
|
||||
|
||||
isAdmin() {
|
||||
return this.user && this.user.hasId() && this.user.RoleAdmin;
|
||||
return this.user && this.user.hasId() && (this.user.Role === "admin" || this.user.SuperAdmin);
|
||||
}
|
||||
|
||||
isAnonymous() {
|
||||
|
||||
@@ -631,11 +631,11 @@ export default {
|
||||
},
|
||||
displayName() {
|
||||
const user = this.$session.getUser();
|
||||
return user.FullName ? user.FullName : user.UserName;
|
||||
return user.DisplayName ? user.DisplayName : user.Username;
|
||||
},
|
||||
accountInfo() {
|
||||
const user = this.$session.getUser();
|
||||
return user.PrimaryEmail ? user.PrimaryEmail : this.$gettext("Account");
|
||||
return user.Email ? user.Email : this.$gettext("Account");
|
||||
},
|
||||
},
|
||||
created() {
|
||||
|
||||
@@ -32,56 +32,72 @@ export class User extends RestModel {
|
||||
getDefaults() {
|
||||
return {
|
||||
UID: "",
|
||||
Address: {},
|
||||
MotherUID: "",
|
||||
FatherUID: "",
|
||||
GlobalUID: "",
|
||||
Slug: "",
|
||||
Username: "",
|
||||
Email: "",
|
||||
Role: "",
|
||||
SuperAdmin: false,
|
||||
CanLogin: false,
|
||||
CanInvite: false,
|
||||
AuthUID: "",
|
||||
AuthSrc: "",
|
||||
WebUID: "",
|
||||
WebDAV: "",
|
||||
AvatarURL: "",
|
||||
AvatarSrc: "",
|
||||
Country: "",
|
||||
TimeZone: "",
|
||||
PlaceID: "",
|
||||
PlaceSrc: "",
|
||||
CellID: "",
|
||||
SubjUID: "",
|
||||
Bio: "",
|
||||
Status: "",
|
||||
URL: "",
|
||||
Phone: "",
|
||||
DisplayName: "",
|
||||
FullName: "",
|
||||
NickName: "",
|
||||
MaidenName: "",
|
||||
Alias: "",
|
||||
ArtistName: "",
|
||||
UserName: "",
|
||||
UserStatus: "",
|
||||
UserDisabled: false,
|
||||
UserSettings: "",
|
||||
PrimaryEmail: "",
|
||||
EmailConfirmed: false,
|
||||
BackupEmail: "",
|
||||
PersonURL: "",
|
||||
PersonPhone: "",
|
||||
PersonStatus: "",
|
||||
PersonAvatar: "",
|
||||
PersonLocation: "",
|
||||
PersonBio: "",
|
||||
BusinessURL: "",
|
||||
BusinessPhone: "",
|
||||
BusinessEmail: "",
|
||||
Artist: false,
|
||||
Favorite: false,
|
||||
Hidden: false,
|
||||
Private: false,
|
||||
Excluded: false,
|
||||
CompanyName: "",
|
||||
DepartmentName: "",
|
||||
JobTitle: "",
|
||||
BusinessURL: "",
|
||||
BusinessPhone: "",
|
||||
BusinessEmail: "",
|
||||
BackupEmail: "",
|
||||
BirthYear: -1,
|
||||
BirthMonth: -1,
|
||||
BirthDay: -1,
|
||||
TermsAccepted: false,
|
||||
IsArtist: false,
|
||||
IsSubject: false,
|
||||
RoleAdmin: false,
|
||||
RoleGuest: false,
|
||||
RoleChild: false,
|
||||
RoleFamily: false,
|
||||
RoleFriend: false,
|
||||
WebDAV: false,
|
||||
StoragePath: "",
|
||||
CanInvite: false,
|
||||
FileRoot: "",
|
||||
FilePath: "",
|
||||
InviteToken: "",
|
||||
InvitedBy: "",
|
||||
DownloadToken: "",
|
||||
PreviewToken: "",
|
||||
ConfirmedAt: "",
|
||||
TermsAccepted: "",
|
||||
CreatedAt: "",
|
||||
UpdatedAt: "",
|
||||
DeletedAt: "",
|
||||
};
|
||||
}
|
||||
|
||||
getEntityName() {
|
||||
return this.FullName ? this.FullName : this.UserName;
|
||||
if (this.DisplayName) {
|
||||
return this.DisplayName;
|
||||
}
|
||||
|
||||
if (this.FullName) {
|
||||
return this.FullName;
|
||||
}
|
||||
|
||||
return this.Name;
|
||||
}
|
||||
|
||||
getRegisterForm() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-container fluid fill-height class="auth-login wallpaper background-welcome pa-3" :style="wallpaper()">
|
||||
<v-container fluid fill-height class="auth-login wallpaper background-welcome pa-4" :style="wallpaper()">
|
||||
<v-layout align-center justify-center>
|
||||
<v-flex xs12 sm8 md4 xl3 xxl2>
|
||||
<v-form ref="form" dense class="auth-login-form" accept-charset="UTF-8" @submit.prevent="login">
|
||||
|
||||
@@ -61,10 +61,10 @@ describe("common/session", () => {
|
||||
const values = {
|
||||
user: {
|
||||
ID: 5,
|
||||
NickName: "Foo",
|
||||
DisplayName: "Foo",
|
||||
FullName: "Max Last",
|
||||
PrimaryEmail: "test@test.com",
|
||||
RoleAdmin: true,
|
||||
Email: "test@test.com",
|
||||
Role: "admin",
|
||||
},
|
||||
};
|
||||
session.setData(values);
|
||||
@@ -72,10 +72,10 @@ describe("common/session", () => {
|
||||
assert.equal(result, "test@test.com");
|
||||
const values2 = {
|
||||
user: {
|
||||
NickName: "Foo",
|
||||
DisplayName: "Foo",
|
||||
FullName: "Max Last",
|
||||
PrimaryEmail: "test@test.com",
|
||||
RoleAdmin: true,
|
||||
Email: "test@test.com",
|
||||
Role: "admin",
|
||||
},
|
||||
};
|
||||
session.setData(values2);
|
||||
@@ -90,25 +90,25 @@ describe("common/session", () => {
|
||||
const values = {
|
||||
user: {
|
||||
ID: 5,
|
||||
NickName: "Foo",
|
||||
DisplayName: "Foo",
|
||||
FullName: "Max Last",
|
||||
PrimaryEmail: "test@test.com",
|
||||
RoleAdmin: true,
|
||||
Email: "test@test.com",
|
||||
Role: "admin",
|
||||
},
|
||||
};
|
||||
session.setData(values);
|
||||
const result = session.getNickName();
|
||||
const result = session.getDisplayName();
|
||||
assert.equal(result, "Foo");
|
||||
const values2 = {
|
||||
user: {
|
||||
NickName: "Bar",
|
||||
DisplayName: "Bar",
|
||||
FullName: "Max Last",
|
||||
PrimaryEmail: "test@test.com",
|
||||
RoleAdmin: true,
|
||||
Email: "test@test.com",
|
||||
Role: "admin",
|
||||
},
|
||||
};
|
||||
session.setData(values2);
|
||||
const result2 = session.getNickName();
|
||||
const result2 = session.getDisplayName();
|
||||
assert.equal(result2, "");
|
||||
session.deleteData();
|
||||
});
|
||||
@@ -119,25 +119,25 @@ describe("common/session", () => {
|
||||
const values = {
|
||||
user: {
|
||||
ID: 5,
|
||||
NickName: "Foo",
|
||||
DisplayName: "Foo",
|
||||
FullName: "Max Last",
|
||||
PrimaryEmail: "test@test.com",
|
||||
RoleAdmin: true,
|
||||
Email: "test@test.com",
|
||||
Role: "admin",
|
||||
},
|
||||
};
|
||||
session.setData(values);
|
||||
const result = session.getFullName();
|
||||
assert.equal(result, "Max Last");
|
||||
const result = session.getDisplayName();
|
||||
assert.equal(result, "Foo");
|
||||
const values2 = {
|
||||
user: {
|
||||
NickName: "Bar",
|
||||
DisplayName: "Bar",
|
||||
FullName: "Max New",
|
||||
PrimaryEmail: "test@test.com",
|
||||
RoleAdmin: true,
|
||||
Email: "test@test.com",
|
||||
Role: "admin",
|
||||
},
|
||||
};
|
||||
session.setData(values2);
|
||||
const result2 = session.getFullName();
|
||||
const result2 = session.getDisplayName();
|
||||
assert.equal(result2, "");
|
||||
session.deleteData();
|
||||
});
|
||||
@@ -148,10 +148,10 @@ describe("common/session", () => {
|
||||
const values = {
|
||||
user: {
|
||||
ID: 5,
|
||||
NickName: "Foo",
|
||||
DisplayName: "Foo",
|
||||
FullName: "Max Last",
|
||||
PrimaryEmail: "test@test.com",
|
||||
RoleAdmin: true,
|
||||
Email: "test@test.com",
|
||||
Role: "admin",
|
||||
},
|
||||
};
|
||||
session.setData(values);
|
||||
@@ -166,10 +166,10 @@ describe("common/session", () => {
|
||||
const values = {
|
||||
user: {
|
||||
ID: 5,
|
||||
NickName: "Foo",
|
||||
DisplayName: "Foo",
|
||||
FullName: "Max Last",
|
||||
PrimaryEmail: "test@test.com",
|
||||
RoleAdmin: true,
|
||||
Email: "test@test.com",
|
||||
Role: "admin",
|
||||
},
|
||||
};
|
||||
session.setData(values);
|
||||
@@ -184,10 +184,10 @@ describe("common/session", () => {
|
||||
const values = {
|
||||
user: {
|
||||
ID: 5,
|
||||
NickName: "Foo",
|
||||
DisplayName: "Foo",
|
||||
FullName: "Max Last",
|
||||
PrimaryEmail: "test@test.com",
|
||||
RoleAdmin: true,
|
||||
Email: "test@test.com",
|
||||
Role: "admin",
|
||||
},
|
||||
};
|
||||
session.setData(values);
|
||||
|
||||
@@ -6,14 +6,14 @@ let assert = chai.assert;
|
||||
|
||||
describe("model/user", () => {
|
||||
it("should get entity name", () => {
|
||||
const values = { ID: 5, FullName: "Max Last", PrimaryEmail: "test@test.com", RoleAdmin: true };
|
||||
const values = { ID: 5, Username: "max", FullName: "Max 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, FullName: "Max Last", PrimaryEmail: "test@test.com", RoleAdmin: true };
|
||||
const values = { ID: 5, Username: "max", FullName: "Max Last", Email: "test@test.com", Role: "admin" };
|
||||
const user = new User(values);
|
||||
const result = user.getId();
|
||||
assert.equal(result, 5);
|
||||
@@ -30,28 +30,28 @@ describe("model/user", () => {
|
||||
});
|
||||
|
||||
it("should get register form", async () => {
|
||||
const values = { ID: 52, FullName: "Max Last" };
|
||||
const values = { ID: 52, Username: "max", FullName: "Max Last" };
|
||||
const user = new User(values);
|
||||
const result = await user.getRegisterForm();
|
||||
assert.equal(result.definition.foo, "register");
|
||||
});
|
||||
|
||||
it("should get profile form", async () => {
|
||||
const values = { ID: 53, FullName: "Max Last" };
|
||||
const values = { ID: 53, Username: "max", FullName: "Max Last" };
|
||||
const user = new User(values);
|
||||
const result = await user.getProfileForm();
|
||||
assert.equal(result.definition.foo, "profile");
|
||||
});
|
||||
|
||||
it("should get change password", async () => {
|
||||
const values = { ID: 54, FullName: "Max Last", PrimaryEmail: "test@test.com", RoleAdmin: true };
|
||||
const values = { ID: 54, Username: "max", FullName: "Max 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 () => {
|
||||
const values = { ID: 55, FullName: "Max Last", PrimaryEmail: "test@test.com", RoleAdmin: true };
|
||||
const values = { ID: 55, Username: "max", FullName: "Max Last", Email: "test@test.com", Role: "admin" };
|
||||
const user = new User(values);
|
||||
assert.equal(user.FullName, "Max Last");
|
||||
await user.saveProfile();
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
)
|
||||
|
||||
// NewApiTest returns new API test helper.
|
||||
@@ -35,7 +35,7 @@ func AuthenticateAdmin(app *gin.Engine, router *gin.RouterGroup) (sessId string)
|
||||
func AuthenticateUser(app *gin.Engine, router *gin.RouterGroup, username string, password string) (sessId string) {
|
||||
CreateSession(router)
|
||||
f := form.Login{
|
||||
UserName: username,
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
loginStr, err := json.Marshal(f)
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
@@ -14,6 +12,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
@@ -31,9 +30,9 @@ func GetConfig(router *gin.RouterGroup) {
|
||||
|
||||
conf := service.Config()
|
||||
|
||||
if s.User.Guest() {
|
||||
if s.User.IsGuest() {
|
||||
c.JSON(http.StatusOK, conf.GuestConfig())
|
||||
} else if s.User.Registered() {
|
||||
} else if s.User.IsRegistered() {
|
||||
c.JSON(http.StatusOK, conf.UserConfig())
|
||||
} else {
|
||||
c.JSON(http.StatusOK, conf.PublicConfig())
|
||||
|
||||
@@ -49,7 +49,7 @@ func Connect(router *gin.RouterGroup) {
|
||||
s := Auth(SessionID(c), acl.ResourceConfigOptions, acl.ActionUpdate)
|
||||
|
||||
if s.Invalid() {
|
||||
log.Errorf("connect: %s not authorized", clean.Log(s.User.UserName))
|
||||
log.Errorf("connect: %s not authorized", clean.Log(s.User.Username))
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@ package api
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/internal/session"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
||||
// POST /api/v1/session
|
||||
@@ -52,11 +52,11 @@ func CreateSession(router *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
// Upgrade from anonymous to guest. Don't downgrade.
|
||||
if data.User.Anonymous() {
|
||||
if data.User.IsAnonymous() {
|
||||
data.User = entity.Guest
|
||||
}
|
||||
} else if f.HasCredentials() {
|
||||
user := entity.FindUserByName(f.UserName)
|
||||
user := entity.FindUserByLogin(f.Username)
|
||||
|
||||
if user == nil {
|
||||
c.AbortWithStatusJSON(400, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
@@ -80,7 +80,7 @@ func CreateSession(router *gin.RouterGroup) {
|
||||
|
||||
AddSessionHeader(c, id)
|
||||
|
||||
if data.User.Anonymous() {
|
||||
if data.User.IsAnonymous() {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "id": id, "data": data, "config": conf.GuestConfig()})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "id": id, "data": data, "config": conf.UserConfig()})
|
||||
@@ -99,7 +99,7 @@ func DeleteSession(router *gin.RouterGroup) {
|
||||
})
|
||||
}
|
||||
|
||||
// Gets session id from HTTP header.
|
||||
// SessionID returns the session id from the HTTP header.
|
||||
func SessionID(c *gin.Context) string {
|
||||
return c.GetHeader("X-Session-ID")
|
||||
}
|
||||
@@ -119,7 +119,7 @@ func Session(id string) session.Data {
|
||||
func Auth(id string, resource acl.Resource, action acl.Action) session.Data {
|
||||
sess := Session(id)
|
||||
|
||||
if acl.Permissions.Deny(resource, sess.User.Role(), action) {
|
||||
if acl.Permissions.Deny(resource, sess.User.AclRole(), action) {
|
||||
return session.Data{}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
)
|
||||
|
||||
func TestCreateSession(t *testing.T) {
|
||||
@@ -14,7 +15,8 @@ func TestCreateSession(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
CreateSession(router)
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"username": "admin", "password": "photoprism"}`)
|
||||
val2 := gjson.Get(r.Body.String(), "data.user.UserName")
|
||||
log.Debugf("BODY: %s", r.Body.String())
|
||||
val2 := gjson.Get(r.Body.String(), "data.user.Username")
|
||||
assert.Equal(t, "admin", val2.String())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
@@ -48,8 +50,8 @@ func TestCreateSession(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
CreateSession(router)
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"username": "alice", "password": "Alice123!"}`)
|
||||
resEmail := gjson.Get(r.Body.String(), "data.user.PrimaryEmail")
|
||||
resUsername := gjson.Get(r.Body.String(), "data.user.UserName")
|
||||
resEmail := gjson.Get(r.Body.String(), "data.user.Email")
|
||||
resUsername := gjson.Get(r.Body.String(), "data.user.Username")
|
||||
assert.Equal(t, "alice@example.com", resEmail.String())
|
||||
assert.Equal(t, "alice", resUsername.String())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
@@ -58,8 +60,8 @@ func TestCreateSession(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
CreateSession(router)
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"username": "bob", "password": "Bobbob123!"}`)
|
||||
resEmail := gjson.Get(r.Body.String(), "data.user.PrimaryEmail")
|
||||
resUsername := gjson.Get(r.Body.String(), "data.user.UserName")
|
||||
resEmail := gjson.Get(r.Body.String(), "data.user.Email")
|
||||
resUsername := gjson.Get(r.Body.String(), "data.user.Username")
|
||||
assert.Equal(t, "bob@example.com", resEmail.String())
|
||||
assert.Equal(t, "bob", resUsername.String())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
@@ -65,9 +66,9 @@ func wsReader(ws *websocket.Conn, writeMutex *sync.Mutex, connId string, conf *c
|
||||
|
||||
var clientConfig config.ClientConfig
|
||||
|
||||
if sess.User.Guest() {
|
||||
if sess.User.IsGuest() {
|
||||
clientConfig = conf.GuestConfig()
|
||||
} else if sess.User.Registered() {
|
||||
} else if sess.User.IsRegistered() {
|
||||
clientConfig = conf.UserConfig()
|
||||
} else {
|
||||
clientConfig = conf.PublicConfig()
|
||||
@@ -150,7 +151,7 @@ func wsWriter(ws *websocket.Conn, writeMutex *sync.Mutex, connId string) {
|
||||
|
||||
wsAuth.mutex.RUnlock()
|
||||
|
||||
if user.Registered() {
|
||||
if user.IsRegistered() {
|
||||
writeMutex.Lock()
|
||||
|
||||
if err := ws.SetWriteDeadline(time.Now().Add(30 * time.Second)); err != nil {
|
||||
|
||||
23
internal/commands/commands_test.go
Normal file
23
internal/commands/commands_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
log = logrus.StandardLogger()
|
||||
log.SetLevel(logrus.TraceLevel)
|
||||
|
||||
c := config.NewTestConfig("commands")
|
||||
|
||||
code := m.Run()
|
||||
|
||||
_ = c.CloseDb()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -5,11 +5,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/leandro-lugaresi/hub"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/capture"
|
||||
)
|
||||
|
||||
@@ -20,13 +19,14 @@ func TestIndexCommand(t *testing.T) {
|
||||
|
||||
s := event.Subscribe("log.info")
|
||||
defer event.Unsubscribe(s)
|
||||
logs := ""
|
||||
|
||||
var l string
|
||||
|
||||
assert.IsType(t, hub.Subscription{}, s)
|
||||
|
||||
go func() {
|
||||
for msg := range s.Receiver {
|
||||
logs += msg.Fields["message"].(string) + "\n"
|
||||
l += msg.Fields["message"].(string) + "\n"
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -39,16 +39,16 @@ func TestIndexCommand(t *testing.T) {
|
||||
}
|
||||
|
||||
if stdout != "" {
|
||||
t.Errorf("unexpected stdout output: %s", stdout)
|
||||
t.Logf("stdout: %s", stdout)
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
if output := logs; output != "" {
|
||||
if l != "" {
|
||||
// Expected index command output.
|
||||
assert.Contains(t, output, "indexing originals")
|
||||
assert.Contains(t, output, "indexed")
|
||||
assert.Contains(t, output, "files")
|
||||
assert.Contains(t, l, "classify: loading labels")
|
||||
assert.NotContains(t, l, "error")
|
||||
assert.Contains(t, l, "found no .ppignore file")
|
||||
} else {
|
||||
t.Fatal("log output missing")
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func passwdAction(ctx *cli.Context) error {
|
||||
|
||||
user := entity.Admin
|
||||
|
||||
log.Infof("please enter a new password for %s (at least 6 characters)\n", clean.Log(user.Username()))
|
||||
log.Infof("please enter a new password for %s (mininum 8 characters)\n", clean.Log(user.UserName()))
|
||||
|
||||
newPassword := getPassword("New Password: ")
|
||||
|
||||
@@ -57,7 +57,7 @@ func passwdAction(ctx *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("changed password for %s\n", clean.Log(user.Username()))
|
||||
log.Infof("changed password for %s\n", clean.Log(user.UserName()))
|
||||
|
||||
conf.Shutdown()
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
||||
// ResetCommand resets the index, clears the cache, and removes sidecar files after confirmation.
|
||||
@@ -166,30 +167,32 @@ func resetAction(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
// resetIndexDb resets the index database schema.
|
||||
func resetIndexDb(conf *config.Config) {
|
||||
func resetIndexDb(c *config.Config) {
|
||||
start := time.Now()
|
||||
|
||||
tables := entity.Entities
|
||||
|
||||
log.Infoln("dropping existing tables")
|
||||
tables.Drop(conf.Db())
|
||||
tables.Drop(c.Db())
|
||||
|
||||
log.Infoln("restoring default schema")
|
||||
entity.InitDb(true, false, nil)
|
||||
|
||||
if conf.AdminPassword() != "" {
|
||||
log.Infoln("restoring initial admin password")
|
||||
entity.Admin.InitPassword(conf.AdminPassword())
|
||||
// Reset admin account?
|
||||
if c.AdminPassword() == "" {
|
||||
log.Warnf("password required to reset admin account")
|
||||
} else if entity.Admin.InitAccount(c.AdminUser(), c.AdminPassword()) {
|
||||
log.Infof("user %s has been restored", clean.LogQuote(c.AdminUser()))
|
||||
}
|
||||
|
||||
log.Infof("database reset completed in %s", time.Since(start))
|
||||
}
|
||||
|
||||
// resetCache removes all cache files and folders.
|
||||
func resetCache(conf *config.Config) {
|
||||
func resetCache(c *config.Config) {
|
||||
start := time.Now()
|
||||
|
||||
matches, err := filepath.Glob(regexp.QuoteMeta(conf.CachePath()) + "/**")
|
||||
matches, err := filepath.Glob(regexp.QuoteMeta(c.CachePath()) + "/**")
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("reset: %s (find cache files)", err)
|
||||
@@ -216,10 +219,10 @@ func resetCache(conf *config.Config) {
|
||||
}
|
||||
|
||||
// resetSidecarJson removes generated *.json sidecar files.
|
||||
func resetSidecarJson(conf *config.Config) {
|
||||
func resetSidecarJson(c *config.Config) {
|
||||
start := time.Now()
|
||||
|
||||
matches, err := filepath.Glob(regexp.QuoteMeta(conf.SidecarPath()) + "/**/*.json")
|
||||
matches, err := filepath.Glob(regexp.QuoteMeta(c.SidecarPath()) + "/**/*.json")
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("reset: %s (find *.json sidecar files)", err)
|
||||
@@ -230,7 +233,7 @@ func resetSidecarJson(conf *config.Config) {
|
||||
log.Infof("removing %d *.json sidecar files", len(matches))
|
||||
|
||||
for _, name := range matches {
|
||||
if err := os.Remove(name); err != nil {
|
||||
if err = os.Remove(name); err != nil {
|
||||
fmt.Print("E")
|
||||
} else {
|
||||
fmt.Print(".")
|
||||
@@ -246,10 +249,10 @@ func resetSidecarJson(conf *config.Config) {
|
||||
}
|
||||
|
||||
// resetSidecarYaml removes generated *.yml files.
|
||||
func resetSidecarYaml(conf *config.Config) {
|
||||
func resetSidecarYaml(c *config.Config) {
|
||||
start := time.Now()
|
||||
|
||||
matches, err := filepath.Glob(regexp.QuoteMeta(conf.SidecarPath()) + "/**/*.yml")
|
||||
matches, err := filepath.Glob(regexp.QuoteMeta(c.SidecarPath()) + "/**/*.yml")
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("reset: %s (find *.yml metadata files)", err)
|
||||
|
||||
@@ -15,63 +15,69 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/report"
|
||||
)
|
||||
|
||||
const UsernameUsage = "unique login identifier"
|
||||
const EmailUsage = "unique email address"
|
||||
const PasswordUsage = "secure login password"
|
||||
|
||||
// UsersCommand registers user management subcommands.
|
||||
var UsersCommand = cli.Command{
|
||||
Name: "users",
|
||||
Usage: "User management subcommands",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
Usage: "Lists registered users",
|
||||
Name: "ls",
|
||||
Aliases: []string{"list"},
|
||||
Usage: "Shows registered users",
|
||||
Flags: report.CliFlags,
|
||||
Action: usersListAction,
|
||||
},
|
||||
{
|
||||
Name: "add",
|
||||
Usage: "Adds a new user",
|
||||
Aliases: []string{"create"},
|
||||
Usage: "Adds a new user account",
|
||||
Action: usersAddAction,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "fullname, n",
|
||||
Usage: "full name of the new user",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "username, u",
|
||||
Usage: "unique username",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "password, p",
|
||||
Usage: "sets the users password",
|
||||
Usage: UsernameUsage,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "email, m",
|
||||
Usage: "sets the users email",
|
||||
Usage: EmailUsage,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "password, p",
|
||||
Usage: PasswordUsage,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update",
|
||||
Usage: "Updates user information",
|
||||
Name: "mod",
|
||||
Aliases: []string{"update"},
|
||||
Usage: "Updates a user account",
|
||||
Action: usersUpdateAction,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "fullname, n",
|
||||
Usage: "full name of the new user",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "password, p",
|
||||
Usage: "sets the users password",
|
||||
Name: "username, u",
|
||||
Usage: UsernameUsage,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "email, m",
|
||||
Usage: "sets the users email",
|
||||
Usage: EmailUsage,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "password, p",
|
||||
Usage: PasswordUsage,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Usage: "Removes an existing user",
|
||||
Name: "rm",
|
||||
Aliases: []string{"delete"},
|
||||
Usage: "Removes a user account",
|
||||
Action: usersDeleteAction,
|
||||
ArgsUsage: "[username]",
|
||||
},
|
||||
@@ -82,49 +88,43 @@ func usersAddAction(ctx *cli.Context) error {
|
||||
return callWithDependencies(ctx, func(conf *config.Config) error {
|
||||
|
||||
uc := form.UserCreate{
|
||||
UserName: strings.TrimSpace(ctx.String("username")),
|
||||
FullName: strings.TrimSpace(ctx.String("fullname")),
|
||||
Username: strings.TrimSpace(ctx.String("username")),
|
||||
Email: strings.TrimSpace(ctx.String("email")),
|
||||
Password: strings.TrimSpace(ctx.String("password")),
|
||||
}
|
||||
|
||||
interactive := true
|
||||
|
||||
if uc.UserName != "" && uc.Password != "" {
|
||||
if uc.Username != "" && uc.Password != "" {
|
||||
log.Debugf("creating user in non-interactive mode")
|
||||
interactive = false
|
||||
}
|
||||
|
||||
if interactive && uc.FullName == "" {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Full Name",
|
||||
}
|
||||
res, err := prompt.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uc.FullName = strings.TrimSpace(res)
|
||||
}
|
||||
|
||||
if interactive && uc.UserName == "" {
|
||||
if interactive && uc.Username == "" {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Username",
|
||||
}
|
||||
|
||||
res, err := prompt.Run()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uc.UserName = strings.TrimSpace(res)
|
||||
|
||||
uc.Username = strings.TrimSpace(res)
|
||||
}
|
||||
|
||||
if interactive && uc.Email == "" {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "E-Mail",
|
||||
Label: "Email",
|
||||
}
|
||||
|
||||
res, err := prompt.Run()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uc.Email = strings.TrimSpace(res)
|
||||
}
|
||||
|
||||
@@ -176,24 +176,24 @@ func usersAddAction(ctx *cli.Context) error {
|
||||
|
||||
func usersDeleteAction(ctx *cli.Context) error {
|
||||
return callWithDependencies(ctx, func(conf *config.Config) error {
|
||||
userName := strings.TrimSpace(ctx.Args().First())
|
||||
login := strings.TrimSpace(ctx.Args().First())
|
||||
|
||||
if userName == "" {
|
||||
if login == "" {
|
||||
return errors.New("please provide a username")
|
||||
}
|
||||
|
||||
actionPrompt := promptui.Prompt{
|
||||
Label: fmt.Sprintf("Delete %s?", clean.Log(userName)),
|
||||
Label: fmt.Sprintf("Delete %s?", clean.Log(login)),
|
||||
IsConfirm: true,
|
||||
}
|
||||
|
||||
if _, err := actionPrompt.Run(); err == nil {
|
||||
if m := entity.FindUserByName(userName); m == nil {
|
||||
return errors.New("user not found")
|
||||
if m := entity.FindUserByLogin(login); m == nil {
|
||||
return errors.New("login name not found")
|
||||
} else if err := m.Delete(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
log.Infof("%s deleted", clean.Log(userName))
|
||||
log.Infof("%s deleted", clean.LogQuote(login))
|
||||
}
|
||||
} else {
|
||||
log.Infof("keeping user")
|
||||
@@ -205,52 +205,54 @@ func usersDeleteAction(ctx *cli.Context) error {
|
||||
|
||||
func usersListAction(ctx *cli.Context) error {
|
||||
return callWithDependencies(ctx, func(conf *config.Config) error {
|
||||
cols := []string{"UID", "Role", "Username", "Email", "Display Name"}
|
||||
|
||||
users := query.RegisteredUsers()
|
||||
rows := make([][]string, len(users))
|
||||
|
||||
log.Infof("found %s", english.Plural(len(users), "user", "users"))
|
||||
|
||||
fmt.Printf("%-4s %-16s %-16s %-16s\n", "ID", "LOGIN", "NAME", "EMAIL")
|
||||
|
||||
for _, user := range users {
|
||||
fmt.Printf("%-4d %-16s %-16s %-16s", user.ID, user.Username(), user.FullName, user.PrimaryEmail)
|
||||
fmt.Printf("\n")
|
||||
for i, user := range users {
|
||||
rows[i] = []string{user.UserUID, user.AclRole().String(), user.UserName(), user.UserEmail(), user.RealName()}
|
||||
}
|
||||
|
||||
return nil
|
||||
result, err := report.Render(rows, cols, report.CliFormat(ctx))
|
||||
|
||||
fmt.Println(result)
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func usersUpdateAction(ctx *cli.Context) error {
|
||||
return callWithDependencies(ctx, func(conf *config.Config) error {
|
||||
username := ctx.Args().First()
|
||||
if username == "" {
|
||||
return errors.New("pass username as argument")
|
||||
login := ctx.Args().First()
|
||||
|
||||
if login == "" {
|
||||
return errors.New("please provide the username as argument")
|
||||
}
|
||||
|
||||
u := entity.FindUserByName(username)
|
||||
u := entity.FindUserByLogin(login)
|
||||
if u == nil {
|
||||
return errors.New("user not found")
|
||||
return errors.New("username not found")
|
||||
}
|
||||
|
||||
uc := form.UserCreate{
|
||||
FullName: strings.TrimSpace(ctx.String("fullname")),
|
||||
Username: strings.TrimSpace(ctx.String("username")),
|
||||
Email: strings.TrimSpace(ctx.String("email")),
|
||||
Password: strings.TrimSpace(ctx.String("password")),
|
||||
}
|
||||
|
||||
if ctx.IsSet("email") && len(uc.Email) > 0 {
|
||||
u.Email = uc.Email
|
||||
}
|
||||
|
||||
if ctx.IsSet("password") {
|
||||
err := u.SetPassword(uc.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("password successfully changed: %s\n", clean.Log(u.Username()))
|
||||
}
|
||||
|
||||
if ctx.IsSet("fullname") {
|
||||
u.FullName = uc.FullName
|
||||
}
|
||||
|
||||
if ctx.IsSet("email") && len(uc.Email) > 0 {
|
||||
u.PrimaryEmail = uc.Email
|
||||
fmt.Printf("password successfully changed: %s\n", clean.Log(u.UserName()))
|
||||
}
|
||||
|
||||
if err := u.Validate(); err != nil {
|
||||
@@ -261,7 +263,7 @@ func usersUpdateAction(ctx *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("user successfully updated: %s\n", clean.Log(u.Username()))
|
||||
fmt.Printf("user account successfully updated: %s\n", clean.Log(u.UserName()))
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -112,10 +112,12 @@ func NewConfig(ctx *cli.Context) *Config {
|
||||
|
||||
// Initialize package extensions.
|
||||
for _, ext := range Extensions() {
|
||||
start := time.Now()
|
||||
|
||||
if err := ext.init(c); err != nil {
|
||||
log.Warnf("config: failed to initialize extension %s (%s)", clean.Log(ext.name), err)
|
||||
log.Warnf("config: %s in %s extension[%s]", err, clean.Log(ext.name), time.Since(start))
|
||||
} else {
|
||||
log.Debugf("config: extension %s initialized", clean.Log(ext.name))
|
||||
log.Debugf("config: %s extension loaded [%s]", clean.Log(ext.name), time.Since(start))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -20,9 +21,20 @@ func isBcrypt(s string) bool {
|
||||
return b
|
||||
}
|
||||
|
||||
// AdminUser returns the admin username.
|
||||
func (c *Config) AdminUser() string {
|
||||
c.options.AdminUser = clean.Login(c.options.AdminUser)
|
||||
|
||||
if c.options.AdminUser == "" {
|
||||
c.options.AdminUser = "admin"
|
||||
}
|
||||
|
||||
return c.options.AdminUser
|
||||
}
|
||||
|
||||
// AdminPassword returns the initial admin password.
|
||||
func (c *Config) AdminPassword() string {
|
||||
return c.options.AdminPassword
|
||||
return clean.Password(c.options.AdminPassword)
|
||||
}
|
||||
|
||||
// Public checks if app runs in public mode and requires no authentication.
|
||||
|
||||
@@ -8,6 +8,9 @@ const ApiUri = "/api/v1"
|
||||
// StaticUri is the relative path for serving static content.
|
||||
const StaticUri = "/static"
|
||||
|
||||
// CustomStaticUri is the relative path for serving custom static content.
|
||||
const CustomStaticUri = "/c/static"
|
||||
|
||||
// MsgSponsor and MsgSignUp provide sponsorship info messages;
|
||||
// SignUpURL a signup link.
|
||||
const MsgSponsor = "PhotoPrism® needs your support!"
|
||||
|
||||
@@ -108,8 +108,10 @@ func (c *Config) WallpaperUri() string {
|
||||
// Valid URI? Local file?
|
||||
if p := clean.Path(c.options.WallpaperUri); p == "" {
|
||||
c.options.WallpaperUri = ""
|
||||
} else if fs.FileExists(filepath.Join(c.StaticPath(), assetPath, p)) {
|
||||
} else if fs.FileExists(path.Join(c.StaticPath(), assetPath, p)) {
|
||||
c.options.WallpaperUri = path.Join(c.StaticUri(), assetPath, p)
|
||||
} else if fs.FileExists(c.CustomStaticFile(path.Join(assetPath, p))) {
|
||||
c.options.WallpaperUri = path.Join(c.CustomStaticUri(), assetPath, p)
|
||||
} else {
|
||||
c.options.WallpaperUri = ""
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
@@ -270,7 +272,12 @@ func (c *Config) MigrateDb(runFailed bool, ids []string) {
|
||||
entity.SetDbProvider(c)
|
||||
entity.InitDb(true, runFailed, ids)
|
||||
|
||||
entity.Admin.InitPassword(c.AdminPassword())
|
||||
// Init admin account?
|
||||
if c.AdminPassword() == "" {
|
||||
log.Warnf("config: password required to initialize %s account", clean.LogQuote(c.AdminUser()))
|
||||
} else if entity.Admin.InitAccount(c.AdminUser(), c.AdminPassword()) {
|
||||
log.Infof("config: %s account has been initialized", clean.LogQuote(c.AdminUser()))
|
||||
}
|
||||
|
||||
go entity.SaveErrorMessages()
|
||||
}
|
||||
@@ -281,7 +288,11 @@ func (c *Config) InitTestDb() {
|
||||
entity.SetDbProvider(c)
|
||||
entity.ResetTestFixtures()
|
||||
|
||||
entity.Admin.InitPassword(c.AdminPassword())
|
||||
if c.AdminPassword() == "" {
|
||||
// Do nothing.
|
||||
} else if entity.Admin.InitAccount(c.AdminUser(), c.AdminPassword()) {
|
||||
log.Debugf("config: %s account has been initialized", clean.LogQuote(c.AdminUser()))
|
||||
}
|
||||
|
||||
go entity.SaveErrorMessages()
|
||||
}
|
||||
|
||||
@@ -424,6 +424,35 @@ func (c *Config) CustomAssetsPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// CustomStaticPath returns the custom static assets' path.
|
||||
func (c *Config) CustomStaticPath() string {
|
||||
if dir := c.CustomAssetsPath(); dir == "" {
|
||||
return ""
|
||||
} else if dir = filepath.Join(dir, "static"); !fs.PathExists(dir) {
|
||||
return ""
|
||||
} else {
|
||||
return dir
|
||||
}
|
||||
}
|
||||
|
||||
// CustomStaticFile returns the path to a custom static file.
|
||||
func (c *Config) CustomStaticFile(fileName string) string {
|
||||
if dir := c.CustomStaticPath(); dir == "" {
|
||||
return ""
|
||||
} else {
|
||||
return filepath.Join(dir, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
// CustomStaticUri returns the URI to a custom static resource.
|
||||
func (c *Config) CustomStaticUri() string {
|
||||
if dir := c.CustomAssetsPath(); dir == "" {
|
||||
return ""
|
||||
} else {
|
||||
return c.CdnUrl(c.BaseUri(CustomStaticUri))
|
||||
}
|
||||
}
|
||||
|
||||
// LocalesPath returns the translation locales path.
|
||||
func (c *Config) LocalesPath() string {
|
||||
return filepath.Join(c.AssetsPath(), "locales")
|
||||
|
||||
@@ -14,6 +14,7 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
||||
rows = [][]string{
|
||||
// Authentication.
|
||||
{"auth-mode", fmt.Sprintf("%s", c.AuthMode())},
|
||||
{"admin-user", c.AdminUser()},
|
||||
{"admin-password", strings.Repeat("*", utf8.RuneCountInString(c.AdminPassword()))},
|
||||
{"public", fmt.Sprintf("%t", c.Public())},
|
||||
|
||||
@@ -167,8 +168,12 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
||||
{"log-filename", c.LogFilename()},
|
||||
}
|
||||
|
||||
if p := c.CustomAssetsPath(); p != "" {
|
||||
rows = append(rows, []string{"custom-assets-path", p})
|
||||
if v := c.CustomAssetsPath(); v != "" {
|
||||
rows = append(rows, []string{"custom-assets-path", v})
|
||||
}
|
||||
|
||||
if v := c.CustomStaticUri(); v != "" {
|
||||
rows = append(rows, []string{"custom-static-uri", v})
|
||||
}
|
||||
|
||||
return rows, cols
|
||||
|
||||
@@ -211,6 +211,15 @@ func TestConfig_DetectNSFW(t *testing.T) {
|
||||
assert.Equal(t, true, result)
|
||||
}
|
||||
|
||||
func TestConfig_AdminUser(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
c.options.AdminUser = "foo "
|
||||
assert.Equal(t, "foo", c.AdminUser())
|
||||
c.options.AdminUser = " Admin"
|
||||
assert.Equal(t, "admin", c.AdminUser())
|
||||
}
|
||||
|
||||
func TestConfig_AdminPassword(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ type Options struct {
|
||||
PartnerID string `yaml:"-" json:"-" flag:"partner-id"`
|
||||
AuthMode string `yaml:"AuthMode" json:"-" flag:"auth-mode"`
|
||||
Public bool `yaml:"Public" json:"-" flag:"public"`
|
||||
AdminUser string `yaml:"AdminUser" json:"-" flag:"admin-user"`
|
||||
AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"`
|
||||
LogLevel string `yaml:"LogLevel" json:"-" flag:"log-level"`
|
||||
Prod bool `yaml:"Prod" json:"Prod" flag:"prod"`
|
||||
|
||||
@@ -18,6 +18,13 @@ var Flags = CliFlags{
|
||||
Value: "password",
|
||||
EnvVar: "PHOTOPRISM_AUTH_MODE",
|
||||
}},
|
||||
CliFlag{
|
||||
Flag: cli.StringFlag{
|
||||
Name: "admin-user, login",
|
||||
Usage: "admin login `USERNAME`",
|
||||
EnvVar: "PHOTOPRISM_ADMIN_USER",
|
||||
Value: "admin",
|
||||
}},
|
||||
CliFlag{
|
||||
Flag: cli.StringFlag{
|
||||
Name: "admin-password, pw",
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
||||
type Addresses []Address
|
||||
|
||||
// Address represents a postal address.
|
||||
type Address struct {
|
||||
ID int `gorm:"primary_key" json:"ID" yaml:"ID"`
|
||||
CellID string `gorm:"type:VARBINARY(42);index;default:'zz'" json:"CellID" yaml:"CellID"`
|
||||
AddressSrc string `gorm:"type:VARBINARY(8);" json:"Src" yaml:"Src"`
|
||||
AddressLat float32 `gorm:"type:FLOAT;index;" json:"Lat" yaml:"Lat,omitempty"`
|
||||
AddressLng float32 `gorm:"type:FLOAT;index;" json:"Lng" yaml:"Lng,omitempty"`
|
||||
AddressLine1 string `gorm:"size:255;" json:"Line1" yaml:"Line1,omitempty"`
|
||||
AddressLine2 string `gorm:"size:255;" json:"Line2" yaml:"Line2,omitempty"`
|
||||
AddressZip string `gorm:"size:32;" json:"Zip" yaml:"Zip,omitempty"`
|
||||
AddressCity string `gorm:"size:128;" json:"City" yaml:"City,omitempty"`
|
||||
AddressState string `gorm:"size:128;" json:"State" yaml:"State,omitempty"`
|
||||
AddressCountry string `gorm:"type:VARBINARY(2);default:'zz'" json:"Country" yaml:"Country,omitempty"`
|
||||
AddressNotes string `gorm:"type:VARCHAR(1024);" json:"Notes" yaml:"Notes,omitempty"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
||||
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the entity database table name.
|
||||
func (Address) TableName() string {
|
||||
return "addresses"
|
||||
}
|
||||
|
||||
// UnknownAddress may be used as placeholder for an unknown address.
|
||||
var UnknownAddress = Address{
|
||||
CellID: UnknownLocation.ID,
|
||||
AddressSrc: SrcDefault,
|
||||
AddressCountry: UnknownCountry.ID,
|
||||
}
|
||||
|
||||
// CreateUnknownAddress creates the default address if not exists.
|
||||
func CreateUnknownAddress() {
|
||||
UnknownAddress = *FirstOrCreateAddress(&UnknownAddress)
|
||||
}
|
||||
|
||||
// FirstOrCreateAddress returns the existing row, inserts a new row or nil in case of errors.
|
||||
func FirstOrCreateAddress(m *Address) *Address {
|
||||
result := Address{}
|
||||
|
||||
if err := Db().Where("cell_id = ?", m.CellID).First(&result).Error; err == nil {
|
||||
return &result
|
||||
} else if createErr := m.Create(); createErr == nil {
|
||||
return m
|
||||
} else if err := Db().Where("cell_id = ?", m.CellID).First(&result).Error; err == nil {
|
||||
return &result
|
||||
} else {
|
||||
log.Errorf("address: %s (find or create %s)", createErr, m.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns an identifier that can be used in logs.
|
||||
func (m *Address) String() string {
|
||||
return clean.Log(fmt.Sprintf("%s, %s %s, %s", m.AddressLine1, m.AddressZip, m.AddressCity, m.AddressCountry))
|
||||
}
|
||||
|
||||
// Unknown returns true if the address is unknown.
|
||||
func (m *Address) Unknown() bool {
|
||||
return m.ID < 0
|
||||
}
|
||||
|
||||
// Create inserts a new row to the database.
|
||||
func (m *Address) Create() error {
|
||||
return Db().Create(m).Error
|
||||
}
|
||||
|
||||
// Saves the new row to the database.
|
||||
func (m *Address) Save() error {
|
||||
return Db().Save(m).Error
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFirstOrCreateAddress(t *testing.T) {
|
||||
t.Run("existing address", func(t *testing.T) {
|
||||
address := Address{ID: 1234567, AddressLine1: "Line 1", AddressCountry: "DE"}
|
||||
|
||||
result := FirstOrCreateAddress(&address)
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
t.Log(result)
|
||||
|
||||
assert.Equal(t, 1234567, result.ID)
|
||||
})
|
||||
t.Run("not existing address", func(t *testing.T) {
|
||||
address := &Address{}
|
||||
|
||||
result := FirstOrCreateAddress(address)
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
|
||||
t.Log(result)
|
||||
|
||||
assert.Equal(t, 1234568, result.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAddress_String(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
address := Address{ID: 1234567, AddressLine1: "Line 1", AddressLine2: "Line 2", AddressCity: "Berlin", AddressCountry: "DE"}
|
||||
addressString := address.String()
|
||||
assert.Equal(t, "'Line 1, Berlin, DE'", addressString)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAddress_Unknown(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
address := Address{ID: 1234567, AddressLine1: "Line 1", AddressLine2: "Line 2", AddressCity: "Berlin", AddressCountry: "DE"}
|
||||
assert.False(t, address.Unknown())
|
||||
})
|
||||
}
|
||||
71
internal/entity/auth_tokens.go
Normal file
71
internal/entity/auth_tokens.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// Tokens represents a list of auth tokens.
|
||||
type Tokens []Token
|
||||
|
||||
// Token represents a set of authentication tokens.
|
||||
// Lengths for OAuth 2.0 fields, see https://blogs.intuit.com/blog/2020/03/23/increased-lengths-for-oauth-2-0-fields/.
|
||||
type Token struct {
|
||||
ID int `gorm:"primary_key" json:"-" yaml:"-"`
|
||||
TokenUID string `gorm:"type:VARBINARY(42);column:token_uid;unique_index;" json:"UID" yaml:"UID"`
|
||||
TokenName string `gorm:"type:VARCHAR(64);column:token_name;" json:"Name,omitempty" yaml:"Name,omitempty"`
|
||||
TokenType string `gorm:"type:VARCHAR(64);column:token_type;" json:"Type,omitempty" yaml:"Type,omitempty"`
|
||||
UserUID string `gorm:"type:VARBINARY(42);column:user_uid;index;" json:"UserUID" yaml:"UserUID"`
|
||||
Version string `gorm:"type:VARCHAR(32);column:version;default:'oauth20';" json:"Version,omitempty" yaml:"Version,omitempty"`
|
||||
Auth string `gorm:"type:VARBINARY(512);column:auth;" json:"Auth,omitempty" yaml:"Auth,omitempty"`
|
||||
Access string `gorm:"type:VARBINARY(4096);column:access;" json:"Access,omitempty" yaml:"Access,omitempty"`
|
||||
Refresh string `gorm:"type:VARBINARY(512);column:refresh;" json:"Refresh,omitempty" yaml:"Refresh,omitempty"`
|
||||
FileRoot string `gorm:"type:VARBINARY(16);column:file_root;" json:"FileRoot" yaml:"FileRoot,omitempty"`
|
||||
FilePath string `gorm:"type:VARBINARY(500);column:file_path;" json:"FilePath" yaml:"FilePath,omitempty"`
|
||||
CanIndex bool `gorm:"default:false;" json:"CanIndex" yaml:"CanIndex,omitempty"`
|
||||
CanImport bool `gorm:"default:false;" json:"CanImport" yaml:"CanImport,omitempty"`
|
||||
CanUpload bool `gorm:"default:false;" json:"CanUpload" yaml:"CanUpload,omitempty"`
|
||||
CanDownload bool `gorm:"default:false;" json:"CanDownload" yaml:"CanDownload,omitempty"`
|
||||
CanSearch bool `gorm:"default:false;" json:"CanSearch" yaml:"CanSearch,omitempty"`
|
||||
CanShare bool `gorm:"default:false;" json:"CanShare" yaml:"CanShare,omitempty"`
|
||||
CanEdit bool `gorm:"default:false;" json:"CanEdit" yaml:"CanEdit,omitempty"`
|
||||
CanComment bool `gorm:"default:false;" json:"CanComment" yaml:"CanComment,omitempty"`
|
||||
CanDelete bool `gorm:"default:false;" json:"CanDelete" yaml:"CanDelete,CanCreate"`
|
||||
Notes string `gorm:"type:VARCHAR(512);column:notes;" json:"Notes,omitempty" yaml:"Notes,omitempty"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
||||
ExpiresAt *time.Time `json:"ExpiresAt,omitempty" yaml:"ExpiresAt,omitempty"`
|
||||
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the entity database table name.
|
||||
func (Token) TableName() string {
|
||||
return "auth_tokens"
|
||||
}
|
||||
|
||||
// Create new entity in the database.
|
||||
func (m *Token) Create() error {
|
||||
return Db().Create(m).Error
|
||||
}
|
||||
|
||||
// Save entity properties.
|
||||
func (m *Token) Save() error {
|
||||
return Db().Save(m).Error
|
||||
}
|
||||
|
||||
// Updates multiple properties in the database.
|
||||
func (m *Token) Updates(values interface{}) error {
|
||||
return UnscopedDb().Model(m).Updates(values).Error
|
||||
}
|
||||
|
||||
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||
func (m *Token) BeforeCreate(tx *gorm.DB) error {
|
||||
if rnd.ValidID(m.TokenUID, 'u') {
|
||||
return nil
|
||||
}
|
||||
m.TokenUID = rnd.GenerateUID('u')
|
||||
return nil
|
||||
}
|
||||
501
internal/entity/auth_user.go
Normal file
501
internal/entity/auth_user.go
Normal file
@@ -0,0 +1,501 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
var UsernameLength = 3
|
||||
var PasswordLength = 4
|
||||
|
||||
// Users represents a list of users.
|
||||
type Users []User
|
||||
|
||||
// User represents a person that may optionally log in as user.
|
||||
type User struct {
|
||||
ID int `gorm:"primary_key" json:"-" yaml:"-"`
|
||||
UserUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
|
||||
UserSlug string `gorm:"type:VARBINARY(160);index;" json:"Slug" yaml:"Slug,omitempty"`
|
||||
Username string `gorm:"size:64;index;" json:"Username" yaml:"Username,omitempty"`
|
||||
Email string `gorm:"size:255;index;" json:"Email" yaml:"Email,omitempty"`
|
||||
UserRole string `gorm:"size:32;" json:"Role" yaml:"Role,omitempty"`
|
||||
SuperAdmin bool `gorm:"default:false;" json:"SuperAdmin,omitempty" yaml:"SuperAdmin,omitempty"`
|
||||
CanLogin bool `gorm:"default:false;" json:"CanLogin,omitempty" yaml:"CanLogin,omitempty"`
|
||||
CanInvite bool `gorm:"default:false;" json:"CanInvite,omitempty" yaml:"CanInvite,omitempty"`
|
||||
ShareUID string `gorm:"type:VARBINARY(42);index;" json:"ShareUID" yaml:"ShareUID,omitempty"`
|
||||
AuthUID string `gorm:"type:VARBINARY(512);column:auth_uid;" json:"-" yaml:"-"`
|
||||
AuthSrc string `gorm:"type:VARBINARY(64);column:auth_src;" json:"-" yaml:"-"`
|
||||
WebDAV string `gorm:"size:16;column:webdav;" json:"WebDAV,omitempty" yaml:"WebDAV,omitempty"`
|
||||
AvatarURL string `gorm:"type:VARBINARY(255);column:avatar_url;" json:"AvatarURL" yaml:"AvatarURL,omitempty"`
|
||||
AvatarSrc string `gorm:"type:VARBINARY(64);column:avatar_src;" json:"AvatarSrc" yaml:"AvatarSrc,omitempty"`
|
||||
UserCountry string `gorm:"type:VARBINARY(2);" json:"Country" yaml:"Country,omitempty"`
|
||||
UserLocale string `gorm:"type:VARBINARY(64);" json:"Locale" yaml:"Locale,omitempty"`
|
||||
TimeZone string `gorm:"type:VARBINARY(64);default:'';" json:"TimeZone" yaml:"TimeZone,omitempty"`
|
||||
PlaceID string `gorm:"type:VARBINARY(42);index;default:'zz'" json:"PlaceID,omitempty" yaml:"-"`
|
||||
PlaceSrc string `gorm:"type:VARBINARY(8);" json:"PlaceSrc,omitempty" yaml:"PlaceSrc,omitempty"`
|
||||
CellID string `gorm:"type:VARBINARY(42);index;default:'zz'" json:"CellID" yaml:"CellID"`
|
||||
SubjUID string `gorm:"type:VARBINARY(42);index;" json:"SubjUID" yaml:"SubjUID,omitempty"`
|
||||
UserBio string `gorm:"size:255;" json:"Bio,omitempty" yaml:"Bio,omitempty"`
|
||||
UserStatus string `gorm:"size:32;" json:"Status,omitempty" yaml:"Status,omitempty"`
|
||||
UserURL string `gorm:"size:255;column:user_url" json:"URL,omitempty" yaml:"URL,omitempty"`
|
||||
UserPhone string `gorm:"size:32;" json:"Phone,omitempty" yaml:"Phone,omitempty"`
|
||||
FullName string `gorm:"size:128;" json:"FullName" yaml:"FullName,omitempty"`
|
||||
DisplayName string `gorm:"size:64;" json:"DisplayName" yaml:"DisplayName,omitempty"`
|
||||
UserAlias string `gorm:"size:64;" json:"Alias" yaml:"Alias,omitempty"`
|
||||
ArtistName string `gorm:"size:64;" json:"ArtistName,omitempty" yaml:"ArtistName,omitempty"`
|
||||
UserArtist bool `gorm:"default:false;" json:"Artist,omitempty" yaml:"Artist,omitempty"`
|
||||
UserFavorite bool `gorm:"default:false;" json:"Favorite" yaml:"Favorite,omitempty"`
|
||||
UserHidden bool `gorm:"default:false;" json:"Hidden" yaml:"Hidden,omitempty"`
|
||||
UserPrivate bool `gorm:"default:false;" json:"Private" yaml:"Private,omitempty"`
|
||||
UserExcluded bool `gorm:"default:false;" json:"Excluded" yaml:"Excluded,omitempty"`
|
||||
CompanyName string `gorm:"size:128;" json:"CompanyName,omitempty" yaml:"CompanyName,omitempty"`
|
||||
DepartmentName string `gorm:"size:128;" json:"DepartmentName,omitempty" yaml:"DepartmentName,omitempty"`
|
||||
JobTitle string `gorm:"size:64;" json:"JobTitle,omitempty" yaml:"JobTitle,omitempty"`
|
||||
BusinessURL string `gorm:"size:255" json:"BusinessURL,omitempty" yaml:"BusinessURL,omitempty"`
|
||||
BusinessPhone string `gorm:"size:32;" json:"BusinessPhone,omitempty" yaml:"BusinessPhone,omitempty"`
|
||||
BusinessEmail string `gorm:"size:255;" json:"BusinessEmail,omitempty" yaml:"BusinessEmail,omitempty"`
|
||||
BackupEmail string `gorm:"size:255;" json:"BackupEmail,omitempty" yaml:"BackupEmail,omitempty"`
|
||||
BirthYear int `gorm:"default:-1;" json:"BirthYear" yaml:"BirthYear,omitempty"`
|
||||
BirthMonth int `gorm:"default:-1;" json:"BirthMonth" yaml:"BirthMonth,omitempty"`
|
||||
BirthDay int `gorm:"default:-1;" json:"BirthDay" yaml:"BirthDay,omitempty"`
|
||||
FileRoot string `gorm:"type:VARBINARY(16);column:file_root;" json:"FileRoot,omitempty" yaml:"FileRoot,omitempty"`
|
||||
FilePath string `gorm:"type:VARBINARY(500);column:file_path;" json:"FilePath,omitempty" yaml:"FilePath,omitempty"`
|
||||
InviteToken string `gorm:"type:VARBINARY(32);" json:"-" yaml:"-"`
|
||||
InvitedBy string `gorm:"type:VARBINARY(32);" json:"-" yaml:"-"`
|
||||
DownloadToken string `gorm:"column:download_token;type:VARBINARY(128);" json:"-" yaml:"-"`
|
||||
PreviewToken string `gorm:"column:preview_token;type:VARBINARY(128);" json:"-" yaml:"-"`
|
||||
ResetToken string `gorm:"type:VARBINARY(64);" json:"-" yaml:"-"`
|
||||
ConfirmToken string `gorm:"type:VARBINARY(64);" json:"-" yaml:"-"`
|
||||
ConfirmedAt *time.Time `json:"ConfirmedAt,omitempty" yaml:"ConfirmedAt,omitempty"`
|
||||
TermsAccepted *time.Time `json:"TermsAccepted,omitempty" yaml:"TermsAccepted,omitempty"`
|
||||
LoginAttempts int `json:"-" yaml:"-"`
|
||||
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 returns the entity database table name.
|
||||
func (User) TableName() string {
|
||||
return "auth_users"
|
||||
}
|
||||
|
||||
// InitAccount sets the name and password of the initial admin account.
|
||||
func (m *User) InitAccount(login, password string) (updated bool) {
|
||||
if !m.IsRegistered() {
|
||||
log.Warn("only registered users can change their password")
|
||||
return false
|
||||
}
|
||||
|
||||
// Password must not be empty.
|
||||
if password == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Update username as well if needed.
|
||||
if err := m.UpdateName(login); err != nil {
|
||||
log.Errorf("user: %s", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
existing := FindPassword(m.UserUID)
|
||||
|
||||
if existing != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
pw := NewPassword(m.UserUID, password)
|
||||
|
||||
// Update password in database.
|
||||
if err := pw.Save(); err != nil {
|
||||
log.Error(err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Create new entity in the database.
|
||||
func (m *User) Create() error {
|
||||
return Db().Create(m).Error
|
||||
}
|
||||
|
||||
// Save entity properties.
|
||||
func (m *User) Save() error {
|
||||
return Db().Save(m).Error
|
||||
}
|
||||
|
||||
// Updates multiple properties in the database.
|
||||
func (m *User) Updates(values interface{}) error {
|
||||
return UnscopedDb().Model(m).Updates(values).Error
|
||||
}
|
||||
|
||||
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||
func (m *User) BeforeCreate(scope *gorm.Scope) error {
|
||||
m.UserSlug = m.GenerateSlug()
|
||||
|
||||
if err := scope.SetColumn("UserSlug", m.UserSlug); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rnd.ValidID(m.UserUID, 'u') {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.UserUID = rnd.GenerateUID('u')
|
||||
|
||||
return scope.SetColumn("UserUID", m.UserUID)
|
||||
}
|
||||
|
||||
// GenerateSlug returns an updated slug.
|
||||
func (m *User) GenerateSlug() string {
|
||||
if l := clean.Login(m.Username); l != "" {
|
||||
return txt.Slug(l)
|
||||
} else if m.UserSlug == "" {
|
||||
return rnd.GenerateToken(8)
|
||||
}
|
||||
|
||||
return m.UserSlug
|
||||
}
|
||||
|
||||
// FirstOrCreateUser returns an existing row, inserts a new row, or nil in case of errors.
|
||||
func FirstOrCreateUser(m *User) *User {
|
||||
result := User{}
|
||||
|
||||
m.UserSlug = m.GenerateSlug()
|
||||
|
||||
if err := Db().Where("id = ? OR user_uid = ?", m.ID, m.UserUID).First(&result).Error; err == nil {
|
||||
return &result
|
||||
} else if err := m.Create(); err != nil {
|
||||
log.Debugf("user: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// FindUserByLogin returns an existing user or nil if not found.
|
||||
func FindUserByLogin(s string) *User {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := User{}
|
||||
|
||||
// Find by Login.
|
||||
if name := clean.Login(s); name == "" {
|
||||
return nil
|
||||
} else if err := Db().Where("username = ?", name).First(&result).Error; err == nil {
|
||||
return &result
|
||||
} else {
|
||||
log.Debugf("username %s not found", clean.LogQuote(name))
|
||||
}
|
||||
|
||||
// Find by Email.
|
||||
if email := clean.Email(s); email == "" {
|
||||
return nil
|
||||
} else if err := Db().Where("email = ?", email).First(&result).Error; err == nil {
|
||||
return &result
|
||||
} else {
|
||||
log.Debugf("email %s not found", clean.LogQuote(email))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindUserByUID returns an existing user or nil if not found.
|
||||
func FindUserByUID(uid string) *User {
|
||||
if uid == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := User{}
|
||||
|
||||
if err := Db().Where("user_uid = ?", uid).First(&result).Error; err == nil {
|
||||
return &result
|
||||
} else {
|
||||
log.Debugf("user uid %s not found", clean.LogQuote(uid))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Delete marks the entity as deleted.
|
||||
func (m *User) Delete() error {
|
||||
if m.ID <= 1 {
|
||||
return fmt.Errorf("cannot delete system user")
|
||||
}
|
||||
|
||||
return Db().Delete(m).Error
|
||||
}
|
||||
|
||||
// Deleted tests if the entity is marked as deleted.
|
||||
func (m *User) Deleted() bool {
|
||||
if m.DeletedAt == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return !m.DeletedAt.IsZero()
|
||||
}
|
||||
|
||||
// String returns an identifier that can be used in logs.
|
||||
func (m *User) String() string {
|
||||
if n := m.UserName(); n != "" {
|
||||
return clean.Log(n)
|
||||
} else if n = m.RealName(); n != "" {
|
||||
return clean.Log(n)
|
||||
} else if m.UserSlug != "" {
|
||||
return clean.Log(m.UserSlug)
|
||||
}
|
||||
|
||||
return clean.Log(m.UserUID)
|
||||
}
|
||||
|
||||
// UserName returns the login username.
|
||||
func (m *User) UserName() string {
|
||||
return clean.Login(m.Username)
|
||||
}
|
||||
|
||||
// UserEmail returns the login email address.
|
||||
func (m *User) UserEmail() string {
|
||||
switch {
|
||||
case m.Email != "":
|
||||
return m.Email
|
||||
case m.BackupEmail != "":
|
||||
return m.BackupEmail
|
||||
case m.BusinessEmail != "":
|
||||
return m.BusinessEmail
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// RealName returns the user' real name if known.
|
||||
func (m *User) RealName() string {
|
||||
switch {
|
||||
case m.FullName != "":
|
||||
return m.FullName
|
||||
case m.DisplayName != "":
|
||||
return m.DisplayName
|
||||
case m.ArtistName != "":
|
||||
return m.ArtistName
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// SetUsername sets the login username to the specified string.
|
||||
func (m *User) SetUsername(login string) (err error) {
|
||||
login = clean.Login(login)
|
||||
|
||||
// Empty?
|
||||
if login == "" {
|
||||
return fmt.Errorf("new username is empty")
|
||||
}
|
||||
|
||||
// Update username and slug.
|
||||
m.Username = login
|
||||
m.UserSlug = m.GenerateSlug()
|
||||
|
||||
// Update display name.
|
||||
if m.DisplayName == "" || m.DisplayName == DefaultAdminFullName {
|
||||
m.DisplayName = clean.Name(login)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateName changes the login username and saves it to the database.
|
||||
func (m *User) UpdateName(login string) (err error) {
|
||||
if err = m.SetUsername(login); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save to database.
|
||||
return m.Updates(Values{
|
||||
"UserSlug": m.UserSlug,
|
||||
"Username": m.Username,
|
||||
"DisplayName": m.DisplayName,
|
||||
})
|
||||
}
|
||||
|
||||
// AclRole returns the user role for ACL permission checks.
|
||||
func (m *User) AclRole() acl.Role {
|
||||
role := clean.Role(m.UserRole)
|
||||
|
||||
switch {
|
||||
case m.SuperAdmin:
|
||||
return acl.RoleAdmin
|
||||
case role == "":
|
||||
return acl.RoleDefault
|
||||
case acl.RoleAdmin.Equal(role):
|
||||
return acl.RoleAdmin
|
||||
case acl.RoleEditor.Equal(role):
|
||||
return acl.RoleEditor
|
||||
case acl.RoleViewer.Equal(role):
|
||||
return acl.RoleViewer
|
||||
case acl.RoleGuest.Equal(role):
|
||||
return acl.RoleGuest
|
||||
default:
|
||||
return acl.Role(role)
|
||||
}
|
||||
}
|
||||
|
||||
// IsRegistered checks if the user is registered e.g. has a username.
|
||||
func (m *User) IsRegistered() bool {
|
||||
return m.UserName() != "" && rnd.EntityUID(m.UserUID, 'u')
|
||||
}
|
||||
|
||||
// IsAdmin checks if the user is an admin with username.
|
||||
func (m *User) IsAdmin() bool {
|
||||
return m.IsRegistered() && m.AclRole() == acl.RoleAdmin
|
||||
}
|
||||
|
||||
// IsEditor checks if the user is an editor with username.
|
||||
func (m *User) IsEditor() bool {
|
||||
return m.IsRegistered() && m.AclRole() == acl.RoleEditor
|
||||
}
|
||||
|
||||
// IsViewer checks if the user is a viewer with username.
|
||||
func (m *User) IsViewer() bool {
|
||||
return m.IsRegistered() && m.AclRole() == acl.RoleViewer
|
||||
}
|
||||
|
||||
// IsGuest checks if the user is a guest.
|
||||
func (m *User) IsGuest() bool {
|
||||
return m.AclRole() == acl.RoleGuest
|
||||
}
|
||||
|
||||
// IsAnonymous checks if the user is unknown.
|
||||
func (m *User) IsAnonymous() bool {
|
||||
return !rnd.EntityUID(m.UserUID, 'u') || m.ID == UnknownUser.ID || m.UserUID == UnknownUser.UserUID
|
||||
}
|
||||
|
||||
// SetPassword sets a new password stored as hash.
|
||||
func (m *User) SetPassword(password string) error {
|
||||
if !m.IsRegistered() {
|
||||
return fmt.Errorf("only registered users can change their password")
|
||||
}
|
||||
|
||||
if len(password) < PasswordLength {
|
||||
return fmt.Errorf("password must have at least %d characters", PasswordLength)
|
||||
}
|
||||
|
||||
pw := NewPassword(m.UserUID, password)
|
||||
|
||||
return pw.Save()
|
||||
}
|
||||
|
||||
// InvalidPassword returns true if the given password does not match the hash.
|
||||
func (m *User) InvalidPassword(password string) bool {
|
||||
if !m.IsRegistered() {
|
||||
log.Warn("only registered users can change their password")
|
||||
return true
|
||||
}
|
||||
|
||||
if password == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 5 * time.Duration(m.LoginAttempts))
|
||||
|
||||
pw := FindPassword(m.UserUID)
|
||||
|
||||
if pw == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if pw.InvalidPassword(password) {
|
||||
if err := Db().Model(m).UpdateColumn("login_attempts", gorm.Expr("login_attempts + ?", 1)).Error; err != nil {
|
||||
log.Errorf("user: %s (update login attempts)", err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if err := Db().Model(m).Updates(map[string]interface{}{"login_attempts": 0, "login_at": TimeStamp()}).Error; err != nil {
|
||||
log.Errorf("user: %s (update last login)", err)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate Makes sure username and email are unique and meet requirements. Returns error if any property is invalid
|
||||
func (m *User) Validate() error {
|
||||
if m.UserName() == "" {
|
||||
return errors.New("username must not be empty")
|
||||
}
|
||||
|
||||
if len(m.UserName()) < UsernameLength {
|
||||
return fmt.Errorf("username must have at least %d characters", UsernameLength)
|
||||
}
|
||||
|
||||
var err error
|
||||
var resultName = User{}
|
||||
|
||||
if err = Db().Where("username = ? AND id <> ?", m.Username, m.ID).First(&resultName).Error; err == nil {
|
||||
return errors.New("username already exists")
|
||||
} else if err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
// stop here if no email is provided
|
||||
if m.Email == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validate email address.
|
||||
if a, err := mail.ParseAddress(m.Email); err != nil {
|
||||
return err
|
||||
} else {
|
||||
m.Email = a.Address // make sure email will be used without name.
|
||||
}
|
||||
|
||||
var resultMail = User{}
|
||||
|
||||
if err = Db().Where("email = ? AND id <> ?", m.Email, m.ID).First(&resultMail).Error; err == nil {
|
||||
return errors.New("email already exists")
|
||||
} else if err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateWithPassword Creates User with Password in db transaction.
|
||||
func CreateWithPassword(uc form.UserCreate) error {
|
||||
u := &User{
|
||||
Username: uc.Username,
|
||||
Email: uc.Email,
|
||||
SuperAdmin: true,
|
||||
}
|
||||
|
||||
if len(uc.Password) < PasswordLength {
|
||||
return fmt.Errorf("password must have at least %d characters", PasswordLength)
|
||||
}
|
||||
|
||||
if err := u.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return Db().Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(u).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
pw := NewPassword(u.UserUID, uc.Password)
|
||||
if err := tx.Create(&pw).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("added user %s with uid %s", clean.Log(u.UserName()), clean.Log(u.UserUID))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
68
internal/entity/auth_user_default.go
Normal file
68
internal/entity/auth_user_default.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
const DefaultAdminUserName = "admin"
|
||||
const DefaultAdminFullName = "Admin"
|
||||
|
||||
const DisplayNameUnknown = "Public"
|
||||
const DisplayNameGuest = "Guest"
|
||||
|
||||
// Admin is the default admin user.
|
||||
var Admin = User{
|
||||
ID: 1,
|
||||
UserSlug: "admin",
|
||||
Username: DefaultAdminUserName,
|
||||
UserRole: acl.RoleAdmin.String(),
|
||||
DisplayName: DefaultAdminFullName,
|
||||
SuperAdmin: true,
|
||||
CanLogin: true,
|
||||
CanInvite: true,
|
||||
InviteToken: rnd.GenerateToken(8),
|
||||
}
|
||||
|
||||
// UnknownUser is an anonymous, public user without own account.
|
||||
var UnknownUser = User{
|
||||
ID: -1,
|
||||
UserSlug: "1",
|
||||
UserUID: "u000000000000001",
|
||||
UserRole: "",
|
||||
Username: "",
|
||||
DisplayName: DisplayNameUnknown,
|
||||
SuperAdmin: false,
|
||||
CanLogin: false,
|
||||
CanInvite: false,
|
||||
InviteToken: "",
|
||||
}
|
||||
|
||||
// Guest is a user without own account e.g. for link sharing.
|
||||
var Guest = User{
|
||||
ID: -2,
|
||||
UserSlug: "2",
|
||||
UserUID: "u000000000000002",
|
||||
UserRole: acl.RoleGuest.String(),
|
||||
Username: "",
|
||||
DisplayName: DisplayNameGuest,
|
||||
SuperAdmin: false,
|
||||
CanLogin: false,
|
||||
CanInvite: false,
|
||||
InviteToken: "",
|
||||
}
|
||||
|
||||
// CreateDefaultUsers initializes the database with default user accounts.
|
||||
func CreateDefaultUsers() {
|
||||
if user := FirstOrCreateUser(&Admin); user != nil {
|
||||
Admin = *user
|
||||
}
|
||||
|
||||
if user := FirstOrCreateUser(&UnknownUser); user != nil {
|
||||
UnknownUser = *user
|
||||
}
|
||||
|
||||
if user := FirstOrCreateUser(&Guest); user != nil {
|
||||
Guest = *user
|
||||
}
|
||||
}
|
||||
84
internal/entity/auth_user_fixtures.go
Normal file
84
internal/entity/auth_user_fixtures.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
type UserMap map[string]User
|
||||
|
||||
func (m UserMap) Get(name string) User {
|
||||
if result, ok := m[name]; ok {
|
||||
return result
|
||||
}
|
||||
|
||||
return User{}
|
||||
}
|
||||
|
||||
func (m UserMap) Pointer(name string) *User {
|
||||
if result, ok := m[name]; ok {
|
||||
return &result
|
||||
}
|
||||
|
||||
return &User{}
|
||||
}
|
||||
|
||||
var UserFixtures = UserMap{
|
||||
"alice": {
|
||||
ID: 5,
|
||||
UserUID: "uqxetse3cy5eo9z2",
|
||||
UserSlug: "alice",
|
||||
Username: "alice",
|
||||
Email: "alice@example.com",
|
||||
UserRole: acl.RoleAdmin.String(),
|
||||
SuperAdmin: true,
|
||||
DisplayName: "Alice",
|
||||
CanLogin: true,
|
||||
CanInvite: true,
|
||||
InviteToken: rnd.GenerateToken(8),
|
||||
},
|
||||
"bob": {
|
||||
ID: 7,
|
||||
UserUID: "uqxc08w3d0ej2283",
|
||||
UserSlug: "bob",
|
||||
Username: "bob",
|
||||
Email: "bob@example.com",
|
||||
UserRole: acl.RoleEditor.String(),
|
||||
SuperAdmin: false,
|
||||
DisplayName: "Bob",
|
||||
CanLogin: true,
|
||||
CanInvite: false,
|
||||
},
|
||||
"friend": {
|
||||
ID: 8,
|
||||
UserUID: "uqxqg7i1kperxvu7",
|
||||
UserSlug: "friend",
|
||||
Username: "friend",
|
||||
Email: "friend@example.com",
|
||||
UserRole: acl.RoleViewer.String(),
|
||||
SuperAdmin: false,
|
||||
DisplayName: "Guy Friend",
|
||||
CanLogin: true,
|
||||
CanInvite: false,
|
||||
},
|
||||
"deleted": {
|
||||
ID: 10000008,
|
||||
UserUID: "uqxqg7i1kperxvu8",
|
||||
UserSlug: "deleted",
|
||||
Username: "deleted",
|
||||
Email: "",
|
||||
DisplayName: "Deleted User",
|
||||
SuperAdmin: false,
|
||||
UserRole: acl.RoleGuest.String(),
|
||||
CanLogin: false,
|
||||
CanInvite: false,
|
||||
DeletedAt: &deleteTime,
|
||||
},
|
||||
}
|
||||
|
||||
// CreateUserFixtures inserts known entities into the database for testing.
|
||||
func CreateUserFixtures() {
|
||||
for _, entity := range UserFixtures {
|
||||
Db().Create(&entity)
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,14 @@ import (
|
||||
func TestUserMap_Get(t *testing.T) {
|
||||
t.Run("get existing user", func(t *testing.T) {
|
||||
r := UserFixtures.Get("alice")
|
||||
assert.Equal(t, "alice", r.UserName)
|
||||
assert.Equal(t, "alice", r.Username())
|
||||
assert.Equal(t, "alice", r.Username)
|
||||
assert.Equal(t, "alice", r.UserName())
|
||||
assert.IsType(t, User{}, r)
|
||||
})
|
||||
t.Run("get not existing user", func(t *testing.T) {
|
||||
r := UserFixtures.Get("monstera")
|
||||
assert.Equal(t, "", r.UserName)
|
||||
assert.Equal(t, "", r.Username())
|
||||
assert.Equal(t, "", r.Username)
|
||||
assert.Equal(t, "", r.UserName())
|
||||
assert.IsType(t, User{}, r)
|
||||
})
|
||||
}
|
||||
@@ -24,12 +24,12 @@ func TestUserMap_Get(t *testing.T) {
|
||||
func TestUserMap_Pointer(t *testing.T) {
|
||||
t.Run("get existing user", func(t *testing.T) {
|
||||
r := UserFixtures.Pointer("alice")
|
||||
assert.Equal(t, "alice", r.UserName)
|
||||
assert.Equal(t, "alice", r.Username)
|
||||
assert.IsType(t, &User{}, r)
|
||||
})
|
||||
t.Run("get not existing user", func(t *testing.T) {
|
||||
r := UserFixtures.Pointer("monstera")
|
||||
assert.Equal(t, "", r.UserName)
|
||||
assert.Equal(t, "", r.Username)
|
||||
assert.IsType(t, &User{}, r)
|
||||
})
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func TestFindUserByName(t *testing.T) {
|
||||
t.Run("admin", func(t *testing.T) {
|
||||
m := FindUserByName("admin")
|
||||
m := FindUserByLogin("admin")
|
||||
|
||||
if m == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
@@ -19,21 +19,25 @@ func TestFindUserByName(t *testing.T) {
|
||||
|
||||
assert.Equal(t, 1, m.ID)
|
||||
assert.NotEmpty(t, m.UserUID)
|
||||
assert.Equal(t, "admin", m.UserName)
|
||||
assert.Equal(t, "admin", m.Username())
|
||||
m.UserName = "Admin "
|
||||
assert.Equal(t, "admin", m.Username())
|
||||
assert.Equal(t, "Admin ", m.UserName)
|
||||
assert.Equal(t, "Admin", m.FullName)
|
||||
assert.True(t, m.RoleAdmin)
|
||||
assert.False(t, m.RoleGuest)
|
||||
assert.False(t, m.UserDisabled)
|
||||
assert.Equal(t, "admin", m.Username)
|
||||
assert.Equal(t, "admin", m.UserName())
|
||||
m.Username = "Admin "
|
||||
assert.Equal(t, "admin", m.UserName())
|
||||
assert.Equal(t, "Admin ", m.Username)
|
||||
assert.Equal(t, "Admin", m.DisplayName)
|
||||
assert.Equal(t, acl.RoleAdmin, m.AclRole())
|
||||
assert.False(t, m.IsEditor())
|
||||
assert.False(t, m.IsViewer())
|
||||
assert.False(t, m.IsGuest())
|
||||
assert.True(t, m.SuperAdmin)
|
||||
assert.True(t, m.CanLogin)
|
||||
assert.True(t, m.CanInvite)
|
||||
assert.NotEmpty(t, m.CreatedAt)
|
||||
assert.NotEmpty(t, m.UpdatedAt)
|
||||
})
|
||||
|
||||
t.Run("alice", func(t *testing.T) {
|
||||
m := FindUserByName("alice")
|
||||
m := FindUserByLogin("alice")
|
||||
|
||||
if m == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
@@ -41,18 +45,22 @@ func TestFindUserByName(t *testing.T) {
|
||||
|
||||
assert.Equal(t, 5, m.ID)
|
||||
assert.Equal(t, "uqxetse3cy5eo9z2", m.UserUID)
|
||||
assert.Equal(t, "alice", m.UserName)
|
||||
assert.Equal(t, "Alice", m.FullName)
|
||||
assert.Equal(t, "alice@example.com", m.PrimaryEmail)
|
||||
assert.True(t, m.RoleAdmin)
|
||||
assert.False(t, m.RoleGuest)
|
||||
assert.False(t, m.UserDisabled)
|
||||
assert.Equal(t, "alice", m.Username)
|
||||
assert.Equal(t, "Alice", m.DisplayName)
|
||||
assert.Equal(t, "alice@example.com", m.Email)
|
||||
assert.True(t, m.SuperAdmin)
|
||||
assert.Equal(t, acl.RoleAdmin, m.AclRole())
|
||||
assert.NotEqual(t, acl.RoleGuest, m.AclRole())
|
||||
assert.False(t, m.IsEditor())
|
||||
assert.False(t, m.IsViewer())
|
||||
assert.False(t, m.IsGuest())
|
||||
assert.True(t, m.CanLogin)
|
||||
assert.NotEmpty(t, m.CreatedAt)
|
||||
assert.NotEmpty(t, m.UpdatedAt)
|
||||
})
|
||||
|
||||
t.Run("bob", func(t *testing.T) {
|
||||
m := FindUserByName("bob")
|
||||
m := FindUserByLogin("bob")
|
||||
|
||||
if m == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
@@ -60,18 +68,20 @@ func TestFindUserByName(t *testing.T) {
|
||||
|
||||
assert.Equal(t, 7, m.ID)
|
||||
assert.Equal(t, "uqxc08w3d0ej2283", m.UserUID)
|
||||
assert.Equal(t, "bob", m.UserName)
|
||||
assert.Equal(t, "Bob", m.FullName)
|
||||
assert.Equal(t, "bob@example.com", m.PrimaryEmail)
|
||||
assert.False(t, m.RoleAdmin)
|
||||
assert.False(t, m.RoleGuest)
|
||||
assert.False(t, m.UserDisabled)
|
||||
assert.Equal(t, "bob", m.Username)
|
||||
assert.Equal(t, "Bob", m.DisplayName)
|
||||
assert.Equal(t, "bob@example.com", m.Email)
|
||||
assert.False(t, m.SuperAdmin)
|
||||
assert.True(t, m.IsEditor())
|
||||
assert.False(t, m.IsViewer())
|
||||
assert.False(t, m.IsGuest())
|
||||
assert.True(t, m.CanLogin)
|
||||
assert.NotEmpty(t, m.CreatedAt)
|
||||
assert.NotEmpty(t, m.UpdatedAt)
|
||||
})
|
||||
|
||||
t.Run("unknown", func(t *testing.T) {
|
||||
m := FindUserByName("")
|
||||
m := FindUserByLogin("")
|
||||
|
||||
if m != nil {
|
||||
t.Fatal("result should be nil")
|
||||
@@ -79,7 +89,7 @@ func TestFindUserByName(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
m := FindUserByName("xxx")
|
||||
m := FindUserByLogin("xxx")
|
||||
|
||||
if m != nil {
|
||||
t.Fatal("result should be nil")
|
||||
@@ -87,9 +97,55 @@ func TestFindUserByName(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_Create(t *testing.T) {
|
||||
t.Run("Slug", func(t *testing.T) {
|
||||
var m = User{
|
||||
Username: "example",
|
||||
UserRole: acl.RoleEditor.String(),
|
||||
DisplayName: "Example",
|
||||
SuperAdmin: false,
|
||||
CanLogin: true,
|
||||
}
|
||||
|
||||
if err := m.Create(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "example", m.UserName())
|
||||
assert.Equal(t, "example", m.Username)
|
||||
assert.Equal(t, m.Username, m.UserSlug)
|
||||
|
||||
if err := m.UpdateName("example-editor"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "example-editor", m.UserName())
|
||||
assert.Equal(t, "example-editor", m.Username)
|
||||
assert.Equal(t, m.Username, m.UserSlug)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_SetName(t *testing.T) {
|
||||
t.Run("photoprism", func(t *testing.T) {
|
||||
m := FindUserByLogin("admin")
|
||||
|
||||
if m == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
|
||||
assert.Equal(t, "admin", m.UserName())
|
||||
assert.Equal(t, "admin", m.Username)
|
||||
|
||||
m.SetUsername("photoprism")
|
||||
|
||||
assert.Equal(t, "photoprism", m.UserName())
|
||||
assert.Equal(t, "photoprism", m.Username)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_InvalidPassword(t *testing.T) {
|
||||
t.Run("admin", func(t *testing.T) {
|
||||
m := FindUserByName("admin")
|
||||
m := FindUserByLogin("admin")
|
||||
|
||||
if m == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
@@ -98,7 +154,7 @@ func TestUser_InvalidPassword(t *testing.T) {
|
||||
assert.False(t, m.InvalidPassword("photoprism"))
|
||||
})
|
||||
t.Run("admin invalid password", func(t *testing.T) {
|
||||
m := FindUserByName("admin")
|
||||
m := FindUserByLogin("admin")
|
||||
|
||||
if m == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
@@ -107,7 +163,7 @@ func TestUser_InvalidPassword(t *testing.T) {
|
||||
assert.True(t, m.InvalidPassword("wrong-password"))
|
||||
})
|
||||
t.Run("no password existing", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000010", UserName: "Hans", FullName: ""}
|
||||
p := User{UserUID: "u000000000000010", Username: "Hans", DisplayName: ""}
|
||||
err := p.Save()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -116,11 +172,11 @@ func TestUser_InvalidPassword(t *testing.T) {
|
||||
|
||||
})
|
||||
t.Run("not registered", func(t *testing.T) {
|
||||
p := User{UserUID: "u12", UserName: "", FullName: ""}
|
||||
p := User{UserUID: "u12", Username: "", DisplayName: ""}
|
||||
assert.True(t, p.InvalidPassword("abcdef"))
|
||||
})
|
||||
t.Run("password empty", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000011", UserName: "User", FullName: ""}
|
||||
p := User{UserUID: "u000000000000011", Username: "User", DisplayName: ""}
|
||||
assert.True(t, p.InvalidPassword(""))
|
||||
})
|
||||
}
|
||||
@@ -176,8 +232,8 @@ func TestFindUserByUID(t *testing.T) {
|
||||
|
||||
assert.Equal(t, -2, m.ID)
|
||||
assert.NotEmpty(t, m.UserUID)
|
||||
assert.Equal(t, "", m.UserName)
|
||||
assert.Equal(t, "Guest", m.FullName)
|
||||
assert.Equal(t, "", m.Username)
|
||||
assert.Equal(t, "Guest", m.DisplayName)
|
||||
assert.NotEmpty(t, m.CreatedAt)
|
||||
assert.NotEmpty(t, m.UpdatedAt)
|
||||
})
|
||||
@@ -207,12 +263,15 @@ func TestFindUserByUID(t *testing.T) {
|
||||
|
||||
assert.Equal(t, 5, m.ID)
|
||||
assert.Equal(t, "uqxetse3cy5eo9z2", m.UserUID)
|
||||
assert.Equal(t, "alice", m.Username())
|
||||
assert.Equal(t, "Alice", m.FullName)
|
||||
assert.Equal(t, "alice@example.com", m.PrimaryEmail)
|
||||
assert.True(t, m.RoleAdmin)
|
||||
assert.False(t, m.RoleGuest)
|
||||
assert.False(t, m.UserDisabled)
|
||||
assert.Equal(t, "alice", m.UserName())
|
||||
assert.Equal(t, "Alice", m.DisplayName)
|
||||
assert.Equal(t, "alice@example.com", m.Email)
|
||||
assert.True(t, m.SuperAdmin)
|
||||
assert.True(t, m.IsAdmin())
|
||||
assert.False(t, m.IsEditor())
|
||||
assert.False(t, m.IsViewer())
|
||||
assert.False(t, m.IsGuest())
|
||||
assert.True(t, m.CanLogin)
|
||||
assert.NotEmpty(t, m.CreatedAt)
|
||||
assert.NotEmpty(t, m.UpdatedAt)
|
||||
})
|
||||
@@ -226,12 +285,15 @@ func TestFindUserByUID(t *testing.T) {
|
||||
|
||||
assert.Equal(t, 7, m.ID)
|
||||
assert.Equal(t, "uqxc08w3d0ej2283", m.UserUID)
|
||||
assert.Equal(t, "bob", m.UserName)
|
||||
assert.Equal(t, "Bob", m.FullName)
|
||||
assert.Equal(t, "bob@example.com", m.PrimaryEmail)
|
||||
assert.False(t, m.RoleAdmin)
|
||||
assert.False(t, m.RoleGuest)
|
||||
assert.False(t, m.UserDisabled)
|
||||
assert.Equal(t, "bob", m.Username)
|
||||
assert.Equal(t, "Bob", m.DisplayName)
|
||||
assert.Equal(t, "bob@example.com", m.Email)
|
||||
assert.False(t, m.SuperAdmin)
|
||||
assert.False(t, m.IsAdmin())
|
||||
assert.True(t, m.IsEditor())
|
||||
assert.False(t, m.IsViewer())
|
||||
assert.False(t, m.IsGuest())
|
||||
assert.True(t, m.CanLogin)
|
||||
assert.NotEmpty(t, m.CreatedAt)
|
||||
assert.NotEmpty(t, m.UpdatedAt)
|
||||
})
|
||||
@@ -244,10 +306,12 @@ func TestFindUserByUID(t *testing.T) {
|
||||
|
||||
assert.Equal(t, 8, m.ID)
|
||||
assert.Equal(t, "uqxqg7i1kperxvu7", m.UserUID)
|
||||
assert.False(t, m.RoleAdmin)
|
||||
assert.False(t, m.RoleGuest)
|
||||
assert.True(t, m.RoleFriend)
|
||||
assert.True(t, m.UserDisabled)
|
||||
assert.False(t, m.SuperAdmin)
|
||||
assert.False(t, m.IsAdmin())
|
||||
assert.False(t, m.IsEditor())
|
||||
assert.True(t, m.IsViewer())
|
||||
assert.False(t, m.IsGuest())
|
||||
assert.True(t, m.CanLogin)
|
||||
assert.NotEmpty(t, m.CreatedAt)
|
||||
assert.NotEmpty(t, m.UpdatedAt)
|
||||
})
|
||||
@@ -256,75 +320,79 @@ func TestFindUserByUID(t *testing.T) {
|
||||
|
||||
func TestUser_String(t *testing.T) {
|
||||
t.Run("UID", func(t *testing.T) {
|
||||
p := User{UserUID: "abc123", UserName: "", FullName: ""}
|
||||
p := User{UserUID: "abc123", Username: "", DisplayName: ""}
|
||||
assert.Equal(t, "abc123", p.String())
|
||||
})
|
||||
t.Run("DisplayName", func(t *testing.T) {
|
||||
p := User{UserUID: "abc123", UserName: "", FullName: "Test"}
|
||||
t.Run("FullName", func(t *testing.T) {
|
||||
p := User{UserUID: "abc123", Username: "", DisplayName: "Test"}
|
||||
assert.Equal(t, "Test", p.String())
|
||||
})
|
||||
t.Run("UserName", func(t *testing.T) {
|
||||
p := User{UserUID: "abc123", UserName: "Super-User ", FullName: "Test"}
|
||||
t.Run("Username", func(t *testing.T) {
|
||||
p := User{UserUID: "abc123", Username: "Super-User ", DisplayName: "Test"}
|
||||
assert.Equal(t, "super-user", p.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_Admin(t *testing.T) {
|
||||
t.Run("true", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: "", RoleAdmin: true}
|
||||
assert.True(t, p.Admin())
|
||||
t.Run("SuperAdmin", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000008", Username: "Hanna", DisplayName: "", SuperAdmin: true}
|
||||
assert.True(t, p.IsAdmin())
|
||||
})
|
||||
t.Run("RoleAdmin", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000008", Username: "Hanna", DisplayName: "", UserRole: acl.RoleAdmin.String()}
|
||||
assert.True(t, p.IsAdmin())
|
||||
})
|
||||
t.Run("false", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: "", RoleAdmin: false}
|
||||
assert.False(t, p.Admin())
|
||||
p := User{UserUID: "u000000000000008", Username: "Hanna", DisplayName: "", SuperAdmin: false, UserRole: ""}
|
||||
assert.False(t, p.IsAdmin())
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_Anonymous(t *testing.T) {
|
||||
t.Run("true", func(t *testing.T) {
|
||||
p := User{UserUID: "", UserName: "Hanna", FullName: ""}
|
||||
assert.True(t, p.Anonymous())
|
||||
p := User{UserUID: "", Username: "Hanna", DisplayName: ""}
|
||||
assert.True(t, p.IsAnonymous())
|
||||
})
|
||||
t.Run("false", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: "", RoleAdmin: true}
|
||||
assert.False(t, p.Anonymous())
|
||||
p := User{UserUID: "u000000000000008", Username: "Hanna", DisplayName: "", SuperAdmin: true}
|
||||
assert.False(t, p.IsAnonymous())
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_Guest(t *testing.T) {
|
||||
t.Run("true", func(t *testing.T) {
|
||||
p := User{UserUID: "", UserName: "Hanna", FullName: "", RoleGuest: true}
|
||||
assert.True(t, p.Guest())
|
||||
p := User{UserUID: "", Username: "Hanna", DisplayName: "", UserRole: "guest"}
|
||||
assert.True(t, p.IsGuest())
|
||||
})
|
||||
t.Run("false", func(t *testing.T) {
|
||||
p := User{UserUID: "", UserName: "Hanna", FullName: ""}
|
||||
assert.False(t, p.Guest())
|
||||
p := User{UserUID: "", Username: "Hanna", DisplayName: ""}
|
||||
assert.False(t, p.IsGuest())
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_SetPassword(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: ""}
|
||||
p := User{UserUID: "u000000000000008", Username: "Hanna", DisplayName: ""}
|
||||
if err := p.SetPassword("insecure"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
t.Run("not registered", func(t *testing.T) {
|
||||
p := User{UserUID: "", UserName: "Hanna", FullName: ""}
|
||||
p := User{UserUID: "", Username: "Hanna", DisplayName: ""}
|
||||
assert.Error(t, p.SetPassword("insecure"))
|
||||
|
||||
})
|
||||
t.Run("password too short", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: ""}
|
||||
p := User{UserUID: "u000000000000008", Username: "Hanna", DisplayName: ""}
|
||||
assert.Error(t, p.SetPassword("cat"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_InitPassword(t *testing.T) {
|
||||
func TestUser_InitLogin(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000009", UserName: "Hanna", FullName: ""}
|
||||
p := User{UserUID: "u000000000000009", Username: "Hanna", DisplayName: ""}
|
||||
assert.Nil(t, FindPassword("u000000000000009"))
|
||||
p.InitPassword("insecure")
|
||||
p.InitAccount("admin", "insecure")
|
||||
m := FindPassword("u000000000000009")
|
||||
|
||||
if m == nil {
|
||||
@@ -332,7 +400,7 @@ func TestUser_InitPassword(t *testing.T) {
|
||||
}
|
||||
})
|
||||
t.Run("already existing", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000010", UserName: "Hans", FullName: ""}
|
||||
p := User{UserUID: "u000000000000010", Username: "Hans", DisplayName: ""}
|
||||
|
||||
if err := p.Save(); err != nil {
|
||||
t.Logf("cannot user %s: ", err)
|
||||
@@ -343,7 +411,7 @@ func TestUser_InitPassword(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.NotNil(t, FindPassword("u000000000000010"))
|
||||
p.InitPassword("insecure")
|
||||
p.InitAccount("admin", "insecure")
|
||||
m := FindPassword("u000000000000010")
|
||||
|
||||
if m == nil {
|
||||
@@ -351,129 +419,112 @@ func TestUser_InitPassword(t *testing.T) {
|
||||
}
|
||||
})
|
||||
t.Run("not registered", func(t *testing.T) {
|
||||
p := User{UserUID: "u12", UserName: "", FullName: ""}
|
||||
p := User{UserUID: "u12", Username: "", DisplayName: ""}
|
||||
assert.Nil(t, FindPassword("u12"))
|
||||
p.InitPassword("insecure")
|
||||
p.InitAccount("admin", "insecure")
|
||||
assert.Nil(t, FindPassword("u12"))
|
||||
})
|
||||
t.Run("password empty", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000011", UserName: "User", FullName: ""}
|
||||
p := User{UserUID: "u000000000000011", Username: "User", DisplayName: ""}
|
||||
assert.Nil(t, FindPassword("u000000000000011"))
|
||||
p.InitPassword("")
|
||||
p.InitAccount("admin", "")
|
||||
assert.Nil(t, FindPassword("u000000000000011"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_Role(t *testing.T) {
|
||||
t.Run("admin", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: "", RoleAdmin: true}
|
||||
assert.Equal(t, acl.Role("admin"), p.Role())
|
||||
})
|
||||
t.Run("child", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: "", RoleChild: true}
|
||||
assert.Equal(t, acl.Role("child"), p.Role())
|
||||
})
|
||||
t.Run("family", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: "", RoleFamily: true}
|
||||
assert.Equal(t, acl.Role("family"), p.Role())
|
||||
})
|
||||
t.Run("friend", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: "", RoleFriend: true}
|
||||
assert.Equal(t, acl.Role("friend"), p.Role())
|
||||
p := User{UserUID: "u000000000000008", Username: "Hanna", DisplayName: "", SuperAdmin: true, UserRole: acl.RoleAdmin.String()}
|
||||
assert.Equal(t, acl.Role("admin"), p.AclRole())
|
||||
assert.True(t, p.IsAdmin())
|
||||
assert.False(t, p.IsGuest())
|
||||
})
|
||||
t.Run("guest", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: "", RoleGuest: true}
|
||||
assert.Equal(t, acl.Role("guest"), p.Role())
|
||||
p := User{UserUID: "u000000000000008", Username: "Hanna", DisplayName: "", UserRole: acl.RoleGuest.String()}
|
||||
assert.Equal(t, acl.Role("guest"), p.AclRole())
|
||||
assert.False(t, p.IsAdmin())
|
||||
assert.True(t, p.IsGuest())
|
||||
})
|
||||
t.Run("default", func(t *testing.T) {
|
||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: ""}
|
||||
assert.Equal(t, acl.Role("*"), p.Role())
|
||||
p := User{UserUID: "u000000000000008", Username: "Hanna", DisplayName: ""}
|
||||
assert.Equal(t, acl.Role("*"), p.AclRole())
|
||||
assert.False(t, p.IsAdmin())
|
||||
assert.False(t, p.IsGuest())
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_Validate(t *testing.T) {
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
u := &User{
|
||||
AddressID: 1,
|
||||
UserName: "validate",
|
||||
FullName: "Validate",
|
||||
PrimaryEmail: "validate@example.com",
|
||||
Username: "validate",
|
||||
DisplayName: "Validate",
|
||||
Email: "validate@example.com",
|
||||
}
|
||||
err := u.Validate()
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
t.Run("username empty", func(t *testing.T) {
|
||||
u := &User{
|
||||
AddressID: 1,
|
||||
UserName: "",
|
||||
FullName: "Validate",
|
||||
PrimaryEmail: "validate@example.com",
|
||||
Username: "",
|
||||
DisplayName: "Validate",
|
||||
Email: "validate@example.com",
|
||||
}
|
||||
err := u.Validate()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("username too short", func(t *testing.T) {
|
||||
u := &User{
|
||||
AddressID: 1,
|
||||
UserName: "va",
|
||||
FullName: "Validate",
|
||||
PrimaryEmail: "validate@example.com",
|
||||
Username: "va",
|
||||
DisplayName: "Validate",
|
||||
Email: "validate@example.com",
|
||||
}
|
||||
err := u.Validate()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("username not unique", func(t *testing.T) {
|
||||
FirstOrCreateUser(&User{
|
||||
AddressID: 1,
|
||||
UserName: "notunique1",
|
||||
Username: "notunique1",
|
||||
})
|
||||
u := &User{
|
||||
AddressID: 1,
|
||||
UserName: "notunique1",
|
||||
FullName: "Not Unique",
|
||||
PrimaryEmail: "notunique1@example.com",
|
||||
Username: "notunique1",
|
||||
DisplayName: "Not Unique",
|
||||
Email: "notunique1@example.com",
|
||||
}
|
||||
assert.Error(t, u.Validate())
|
||||
})
|
||||
t.Run("email not unique", func(t *testing.T) {
|
||||
FirstOrCreateUser(&User{
|
||||
AddressID: 1,
|
||||
PrimaryEmail: "notunique2@example.com",
|
||||
Email: "notunique2@example.com",
|
||||
})
|
||||
u := &User{
|
||||
AddressID: 1,
|
||||
UserName: "notunique2",
|
||||
FullName: "Not Unique",
|
||||
PrimaryEmail: "notunique2@example.com",
|
||||
Username: "notunique2",
|
||||
Email: "notunique2@example.com",
|
||||
DisplayName: "Not Unique",
|
||||
}
|
||||
assert.Error(t, u.Validate())
|
||||
})
|
||||
t.Run("update user - email not unique", func(t *testing.T) {
|
||||
FirstOrCreateUser(&User{
|
||||
AddressID: 1,
|
||||
UserName: "notunique3",
|
||||
FullName: "Not Unique",
|
||||
PrimaryEmail: "notunique3@example.com",
|
||||
Username: "notunique3",
|
||||
Email: "notunique3@example.com",
|
||||
DisplayName: "Not Unique",
|
||||
})
|
||||
u := FirstOrCreateUser(&User{
|
||||
AddressID: 1,
|
||||
UserName: "notunique30",
|
||||
FullName: "Not Unique",
|
||||
PrimaryEmail: "notunique3@example.com",
|
||||
Username: "notunique30",
|
||||
Email: "notunique3@example.com",
|
||||
DisplayName: "Not Unique",
|
||||
})
|
||||
u.UserName = "notunique3"
|
||||
u.Username = "notunique3"
|
||||
assert.Error(t, u.Validate())
|
||||
})
|
||||
t.Run("primary email empty", func(t *testing.T) {
|
||||
FirstOrCreateUser(&User{
|
||||
AddressID: 1,
|
||||
UserName: "nnomail",
|
||||
Username: "nnomail",
|
||||
})
|
||||
u := &User{
|
||||
AddressID: 1,
|
||||
UserName: "nomail",
|
||||
FullName: "No Mail",
|
||||
PrimaryEmail: "",
|
||||
Username: "nomail",
|
||||
Email: "",
|
||||
DisplayName: "No Mail",
|
||||
}
|
||||
assert.Nil(t, u.Validate())
|
||||
})
|
||||
@@ -482,21 +533,21 @@ func TestUser_Validate(t *testing.T) {
|
||||
func TestCreateWithPassword(t *testing.T) {
|
||||
t.Run("password too short", func(t *testing.T) {
|
||||
u := form.UserCreate{
|
||||
UserName: "thomas1",
|
||||
FullName: "Thomas One",
|
||||
Username: "thomas1",
|
||||
Email: "thomas1@example.com",
|
||||
Password: "hel",
|
||||
}
|
||||
|
||||
err := CreateWithPassword(u)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
u := form.UserCreate{
|
||||
UserName: "thomas2",
|
||||
FullName: "Thomas Two",
|
||||
Username: "thomas2",
|
||||
Email: "thomas2@example.com",
|
||||
Password: "helloworld",
|
||||
}
|
||||
|
||||
err := CreateWithPassword(u)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
@@ -505,22 +556,22 @@ func TestCreateWithPassword(t *testing.T) {
|
||||
func TestDeleteUser(t *testing.T) {
|
||||
t.Run("delete user - success", func(t *testing.T) {
|
||||
u := &User{
|
||||
AddressID: 1,
|
||||
UserName: "thomasdel",
|
||||
FullName: "Thomas Delete",
|
||||
PrimaryEmail: "thomasdel@example.com",
|
||||
Username: "thomasdel",
|
||||
Email: "thomasdel@example.com",
|
||||
DisplayName: "Thomas Delete",
|
||||
}
|
||||
|
||||
u = FirstOrCreateUser(u)
|
||||
err := u.Delete()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("delete user - not in db", func(t *testing.T) {
|
||||
u := &User{
|
||||
AddressID: 1,
|
||||
UserName: "thomasdel2",
|
||||
FullName: "Thomas Delete 2",
|
||||
PrimaryEmail: "thomasdel2@example.com",
|
||||
Username: "thomasdel2",
|
||||
Email: "thomasdel2@example.com",
|
||||
DisplayName: "Thomas Delete 2",
|
||||
}
|
||||
|
||||
err := u.Delete()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
@@ -18,6 +18,11 @@ type Duplicate struct {
|
||||
ModTime int64 `json:"ModTime" yaml:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the entity database table name.
|
||||
func (Duplicate) TableName() string {
|
||||
return "duplicates"
|
||||
}
|
||||
|
||||
// AddDuplicate adds a duplicate.
|
||||
func AddDuplicate(fileName, fileRoot, fileHash string, fileSize, modTime int64) error {
|
||||
if fileName == "" {
|
||||
|
||||
@@ -69,3 +69,11 @@ const (
|
||||
SortOrderCategory = "category"
|
||||
SortOrderSimilar = "similar"
|
||||
)
|
||||
|
||||
// User feature flags.
|
||||
const (
|
||||
IsEnabled = "enabled"
|
||||
IsDisabled = "disabled"
|
||||
CanUpload = "upload"
|
||||
CanDownload = "download"
|
||||
)
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
// CreateDefaultFixtures inserts default fixtures for test and production.
|
||||
func CreateDefaultFixtures() {
|
||||
CreateUnknownAddress()
|
||||
CreateDefaultUsers()
|
||||
CreateUnknownPlace()
|
||||
CreateUnknownLocation()
|
||||
|
||||
@@ -15,31 +15,31 @@ type Tables map[string]interface{}
|
||||
// Entities contains database entities and their table names.
|
||||
var Entities = Tables{
|
||||
migrate.Migration{}.TableName(): &migrate.Migration{},
|
||||
"errors": &Error{},
|
||||
"addresses": &Address{},
|
||||
"users": &User{},
|
||||
"accounts": &Account{},
|
||||
"folders": &Folder{},
|
||||
"duplicates": &Duplicate{},
|
||||
Error{}.TableName(): &Error{},
|
||||
Password{}.TableName(): &Password{},
|
||||
User{}.TableName(): &User{},
|
||||
Token{}.TableName(): &Token{},
|
||||
Account{}.TableName(): &Account{},
|
||||
Folder{}.TableName(): &Folder{},
|
||||
Duplicate{}.TableName(): &Duplicate{},
|
||||
File{}.TableName(): &File{},
|
||||
"files_share": &FileShare{},
|
||||
"files_sync": &FileSync{},
|
||||
FileShare{}.TableName(): &FileShare{},
|
||||
FileSync{}.TableName(): &FileSync{},
|
||||
Photo{}.TableName(): &Photo{},
|
||||
"details": &Details{},
|
||||
Details{}.TableName(): &Details{},
|
||||
Place{}.TableName(): &Place{},
|
||||
Cell{}.TableName(): &Cell{},
|
||||
"cameras": &Camera{},
|
||||
"lenses": &Lens{},
|
||||
"countries": &Country{},
|
||||
"albums": &Album{},
|
||||
"photos_albums": &PhotoAlbum{},
|
||||
"labels": &Label{},
|
||||
"categories": &Category{},
|
||||
"photos_labels": &PhotoLabel{},
|
||||
"keywords": &Keyword{},
|
||||
"photos_keywords": &PhotoKeyword{},
|
||||
"passwords": &Password{},
|
||||
"links": &Link{},
|
||||
Camera{}.TableName(): &Camera{},
|
||||
Lens{}.TableName(): &Lens{},
|
||||
Country{}.TableName(): &Country{},
|
||||
Album{}.TableName(): &Album{},
|
||||
PhotoAlbum{}.TableName(): &PhotoAlbum{},
|
||||
Label{}.TableName(): &Label{},
|
||||
Category{}.TableName(): &Category{},
|
||||
PhotoLabel{}.TableName(): &PhotoLabel{},
|
||||
Keyword{}.TableName(): &Keyword{},
|
||||
PhotoKeyword{}.TableName(): &PhotoKeyword{},
|
||||
Link{}.TableName(): &Link{},
|
||||
Subject{}.TableName(): &Subject{},
|
||||
Face{}.TableName(): &Face{},
|
||||
Marker{}.TableName(): &Marker{},
|
||||
@@ -73,7 +73,15 @@ func (list Tables) WaitForMigration(db *gorm.DB) {
|
||||
|
||||
// Truncate removes all data from tables without dropping them.
|
||||
func (list Tables) Truncate(db *gorm.DB) {
|
||||
for name := range list {
|
||||
var name string
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("migrate: %s in %s (truncate)", r, name)
|
||||
}
|
||||
}()
|
||||
|
||||
for name = range list {
|
||||
if err := db.Exec(fmt.Sprintf("DELETE FROM %s WHERE 1", name)).Error; err == nil {
|
||||
// log.Debugf("entity: removed all data from %s", name)
|
||||
break
|
||||
@@ -85,14 +93,23 @@ func (list Tables) Truncate(db *gorm.DB) {
|
||||
|
||||
// Migrate migrates all database tables of registered entities.
|
||||
func (list Tables) Migrate(db *gorm.DB, runFailed bool, ids []string) {
|
||||
var name string
|
||||
var entity interface{}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("migrate: %s in %s (panic)", r, name)
|
||||
}
|
||||
}()
|
||||
|
||||
if len(ids) == 0 {
|
||||
for name, entity := range list {
|
||||
for name, entity = range list {
|
||||
if err := db.AutoMigrate(entity).Error; err != nil {
|
||||
log.Debugf("migrate: %s (waiting 1s)", err.Error())
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
if err := db.AutoMigrate(entity).Error; err != nil {
|
||||
if err = db.AutoMigrate(entity).Error; err != nil {
|
||||
log.Errorf("migrate: failed migrating %s", clean.Log(name))
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ type Keyword struct {
|
||||
Skip bool
|
||||
}
|
||||
|
||||
// TableName returns the entity database table name.
|
||||
func (Keyword) TableName() string {
|
||||
return "keywords"
|
||||
}
|
||||
|
||||
// NewKeyword registers a new keyword in database
|
||||
func NewKeyword(keyword string) *Keyword {
|
||||
keyword = strings.ToLower(txt.Clip(keyword, txt.ClipKeyword))
|
||||
|
||||
@@ -29,6 +29,11 @@ type Link struct {
|
||||
ModifiedAt time.Time `deepcopier:"skip" json:"ModifiedAt" yaml:"ModifiedAt"`
|
||||
}
|
||||
|
||||
// TableName returns the entity database table name.
|
||||
func (Link) TableName() string {
|
||||
return "links"
|
||||
}
|
||||
|
||||
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||
func (m *Link) BeforeCreate(scope *gorm.Scope) error {
|
||||
if rnd.ValidID(m.LinkUID, 's') {
|
||||
|
||||
@@ -14,6 +14,11 @@ type Password struct {
|
||||
UpdatedAt time.Time `deepcopier:"skip" json:"UpdatedAt"`
|
||||
}
|
||||
|
||||
// TableName returns the entity database table name.
|
||||
func (Password) TableName() string {
|
||||
return "passwords"
|
||||
}
|
||||
|
||||
// NewPassword creates a new password instance.
|
||||
func NewPassword(uid, password string) Password {
|
||||
if uid == "" {
|
||||
|
||||
@@ -1,428 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
var UsernameLength = 3
|
||||
var PasswordLength = 4
|
||||
|
||||
type Users []User
|
||||
|
||||
// User represents a person that may optionally log in as user.
|
||||
type User struct {
|
||||
ID int `gorm:"primary_key" json:"-" yaml:"-"`
|
||||
Address *Address `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false;PRELOAD:true;" json:"Address,omitempty" yaml:"Address,omitempty"`
|
||||
AddressID int `gorm:"default:1" json:"-" yaml:"-"`
|
||||
UserUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
|
||||
MotherUID string `gorm:"type:VARBINARY(42);" json:"MotherUID" yaml:"MotherUID,omitempty"`
|
||||
FatherUID string `gorm:"type:VARBINARY(42);" json:"FatherUID" yaml:"FatherUID,omitempty"`
|
||||
GlobalUID string `gorm:"type:VARBINARY(42);index;" json:"GlobalUID" yaml:"GlobalUID,omitempty"`
|
||||
FullName string `gorm:"size:128;" json:"FullName" yaml:"FullName,omitempty"`
|
||||
NickName string `gorm:"size:64;" json:"NickName" yaml:"NickName,omitempty"`
|
||||
MaidenName string `gorm:"size:64;" json:"MaidenName" yaml:"MaidenName,omitempty"`
|
||||
ArtistName string `gorm:"size:64;" json:"ArtistName" yaml:"ArtistName,omitempty"`
|
||||
UserName string `gorm:"size:64;" json:"UserName" yaml:"UserName,omitempty"`
|
||||
UserStatus string `gorm:"size:32;" json:"UserStatus" yaml:"UserStatus,omitempty"`
|
||||
UserDisabled bool `json:"UserDisabled" yaml:"UserDisabled,omitempty"`
|
||||
UserSettings string `gorm:"type:LONGTEXT;" json:"-" yaml:"-"`
|
||||
PrimaryEmail string `gorm:"size:255;index;" json:"PrimaryEmail" yaml:"PrimaryEmail,omitempty"`
|
||||
EmailConfirmed bool `json:"EmailConfirmed" yaml:"EmailConfirmed,omitempty"`
|
||||
BackupEmail string `gorm:"size:255;" json:"BackupEmail" yaml:"BackupEmail,omitempty"`
|
||||
PersonURL string `gorm:"type:VARBINARY(255);" json:"PersonURL" yaml:"PersonURL,omitempty"`
|
||||
PersonPhone string `gorm:"size:32;" json:"PersonPhone" yaml:"PersonPhone,omitempty"`
|
||||
PersonStatus string `gorm:"size:32;" json:"PersonStatus" yaml:"PersonStatus,omitempty"`
|
||||
PersonAvatar string `gorm:"type:VARBINARY(255);" json:"PersonAvatar" yaml:"PersonAvatar,omitempty"`
|
||||
PersonLocation string `gorm:"size:128;" json:"PersonLocation" yaml:"PersonLocation,omitempty"`
|
||||
PersonBio string `gorm:"type:TEXT;" json:"PersonBio" yaml:"PersonBio,omitempty"`
|
||||
PersonAccounts string `gorm:"type:LONGTEXT;" json:"-" yaml:"-"`
|
||||
BusinessURL string `gorm:"type:VARBINARY(255);" json:"BusinessURL" yaml:"BusinessURL,omitempty"`
|
||||
BusinessPhone string `gorm:"size:32;" json:"BusinessPhone" yaml:"BusinessPhone,omitempty"`
|
||||
BusinessEmail string `gorm:"size:255;" json:"BusinessEmail" yaml:"BusinessEmail,omitempty"`
|
||||
CompanyName string `gorm:"size:128;" json:"CompanyName" yaml:"CompanyName,omitempty"`
|
||||
DepartmentName string `gorm:"size:128;" json:"DepartmentName" yaml:"DepartmentName,omitempty"`
|
||||
JobTitle string `gorm:"size:64;" json:"JobTitle" yaml:"JobTitle,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"`
|
||||
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"`
|
||||
WebDAV bool `gorm:"column:webdav" json:"WebDAV" yaml:"WebDAV,omitempty"`
|
||||
StoragePath string `gorm:"column:storage_path;type:VARBINARY(500);" json:"StoragePath" yaml:"StoragePath,omitempty"`
|
||||
CanInvite bool `json:"CanInvite" yaml:"CanInvite,omitempty"`
|
||||
InviteToken string `gorm:"type:VARBINARY(32);" json:"-" yaml:"-"`
|
||||
InvitedBy string `gorm:"type:VARBINARY(32);" json:"-" yaml:"-"`
|
||||
ConfirmToken string `gorm:"type:VARBINARY(64);" json:"-" yaml:"-"`
|
||||
ResetToken string `gorm:"type:VARBINARY(64);" json:"-" yaml:"-"`
|
||||
ApiToken string `gorm:"column:api_token;type:VARBINARY(128);" json:"-" yaml:"-"`
|
||||
ApiSecret string `gorm:"column:api_secret;type:VARBINARY(128);" json:"-" yaml:"-"`
|
||||
LoginAttempts int `json:"-" yaml:"-"`
|
||||
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 returns the entity database table name.
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
// Admin is the default admin user.
|
||||
var Admin = User{
|
||||
ID: 1,
|
||||
AddressID: 1,
|
||||
UserName: "admin",
|
||||
FullName: "Admin",
|
||||
RoleAdmin: true,
|
||||
UserDisabled: false,
|
||||
}
|
||||
|
||||
// UnknownUser is an anonymous, public user without own account.
|
||||
var UnknownUser = User{
|
||||
ID: -1,
|
||||
AddressID: 1,
|
||||
UserUID: "u000000000000001",
|
||||
UserName: "",
|
||||
FullName: "Anonymous",
|
||||
RoleAdmin: false,
|
||||
RoleGuest: false,
|
||||
UserDisabled: true,
|
||||
}
|
||||
|
||||
// Guest is a user without own account e.g. for link sharing.
|
||||
var Guest = User{
|
||||
ID: -2,
|
||||
AddressID: 1,
|
||||
UserUID: "u000000000000002",
|
||||
UserName: "",
|
||||
FullName: "Guest",
|
||||
RoleAdmin: false,
|
||||
RoleGuest: true,
|
||||
UserDisabled: true,
|
||||
}
|
||||
|
||||
// CreateDefaultUsers initializes the database with default user accounts.
|
||||
func CreateDefaultUsers() {
|
||||
if user := FirstOrCreateUser(&Admin); user != nil {
|
||||
Admin = *user
|
||||
}
|
||||
|
||||
if user := FirstOrCreateUser(&UnknownUser); user != nil {
|
||||
UnknownUser = *user
|
||||
}
|
||||
|
||||
if user := FirstOrCreateUser(&Guest); user != nil {
|
||||
Guest = *user
|
||||
}
|
||||
}
|
||||
|
||||
// Create new entity in the database.
|
||||
func (m *User) Create() error {
|
||||
return Db().Create(m).Error
|
||||
}
|
||||
|
||||
// Save entity properties.
|
||||
func (m *User) Save() error {
|
||||
return Db().Save(m).Error
|
||||
}
|
||||
|
||||
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||
func (m *User) BeforeCreate(tx *gorm.DB) error {
|
||||
if rnd.ValidID(m.UserUID, 'u') {
|
||||
return nil
|
||||
}
|
||||
m.UserUID = rnd.GenerateUID('u')
|
||||
return nil
|
||||
}
|
||||
|
||||
// FirstOrCreateUser returns an existing row, inserts a new row, or nil in case of errors.
|
||||
func FirstOrCreateUser(m *User) *User {
|
||||
result := User{}
|
||||
|
||||
if err := Db().Preload("Address").Where("id = ? OR user_uid = ?", m.ID, m.UserUID).First(&result).Error; err == nil {
|
||||
return &result
|
||||
} else if err := m.Create(); err != nil {
|
||||
log.Debugf("user: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// FindUserByName returns an existing user or nil if not found.
|
||||
func FindUserByName(userName string) *User {
|
||||
userName = clean.Username(userName)
|
||||
|
||||
if userName == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := User{}
|
||||
|
||||
if err := Db().Preload("Address").Where("user_name = ?", userName).First(&result).Error; err == nil {
|
||||
return &result
|
||||
} else {
|
||||
log.Debugf("user %s not found", clean.Log(userName))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// FindUserByUID returns an existing user or nil if not found.
|
||||
func FindUserByUID(uid string) *User {
|
||||
if uid == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := User{}
|
||||
|
||||
if err := Db().Preload("Address").Where("user_uid = ?", uid).First(&result).Error; err == nil {
|
||||
return &result
|
||||
} else {
|
||||
log.Debugf("user %s not found", clean.Log(uid))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Delete marks the entity as deleted.
|
||||
func (m *User) Delete() error {
|
||||
if m.ID <= 1 {
|
||||
return fmt.Errorf("cannot delete system user")
|
||||
}
|
||||
|
||||
return Db().Delete(m).Error
|
||||
}
|
||||
|
||||
// Deleted tests if the entity is marked as deleted.
|
||||
func (m *User) Deleted() bool {
|
||||
if m.DeletedAt == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return !m.DeletedAt.IsZero()
|
||||
}
|
||||
|
||||
// String returns an identifier that can be used in logs.
|
||||
func (m *User) String() string {
|
||||
if n := m.Username(); n != "" {
|
||||
return clean.Log(n)
|
||||
}
|
||||
|
||||
if m.FullName != "" {
|
||||
return clean.Log(m.FullName)
|
||||
}
|
||||
|
||||
return clean.Log(m.UserUID)
|
||||
}
|
||||
|
||||
// Username returns the normalized username.
|
||||
func (m *User) Username() string {
|
||||
return clean.Username(m.UserName)
|
||||
}
|
||||
|
||||
// Registered tests if the user is registered e.g. has a username.
|
||||
func (m *User) Registered() bool {
|
||||
return m.Username() != "" && rnd.EntityUID(m.UserUID, 'u')
|
||||
}
|
||||
|
||||
// Admin returns true if the user is an admin with user name.
|
||||
func (m *User) Admin() bool {
|
||||
return m.Registered() && m.RoleAdmin
|
||||
}
|
||||
|
||||
// Anonymous returns true if the user is unknown.
|
||||
func (m *User) Anonymous() bool {
|
||||
return !rnd.EntityUID(m.UserUID, 'u') || m.ID == UnknownUser.ID || m.UserUID == UnknownUser.UserUID
|
||||
}
|
||||
|
||||
// Guest returns true if the user is a guest.
|
||||
func (m *User) Guest() bool {
|
||||
return m.RoleGuest
|
||||
}
|
||||
|
||||
// SetPassword sets a new password stored as hash.
|
||||
func (m *User) SetPassword(password string) error {
|
||||
if !m.Registered() {
|
||||
return fmt.Errorf("only registered users can change their password")
|
||||
}
|
||||
|
||||
if len(password) < PasswordLength {
|
||||
return fmt.Errorf("password must have at least %d characters", PasswordLength)
|
||||
}
|
||||
|
||||
pw := NewPassword(m.UserUID, password)
|
||||
|
||||
return pw.Save()
|
||||
}
|
||||
|
||||
// InitPassword sets the initial user password stored as hash.
|
||||
func (m *User) InitPassword(password string) {
|
||||
if !m.Registered() {
|
||||
log.Warn("only registered users can change their password")
|
||||
return
|
||||
}
|
||||
|
||||
if password == "" {
|
||||
return
|
||||
}
|
||||
|
||||
existing := FindPassword(m.UserUID)
|
||||
|
||||
if existing != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pw := NewPassword(m.UserUID, password)
|
||||
|
||||
if err := pw.Save(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// InvalidPassword returns true if the given password does not match the hash.
|
||||
func (m *User) InvalidPassword(password string) bool {
|
||||
if !m.Registered() {
|
||||
log.Warn("only registered users can change their password")
|
||||
return true
|
||||
}
|
||||
|
||||
if password == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 5 * time.Duration(m.LoginAttempts))
|
||||
|
||||
pw := FindPassword(m.UserUID)
|
||||
|
||||
if pw == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if pw.InvalidPassword(password) {
|
||||
if err := Db().Model(m).UpdateColumn("login_attempts", gorm.Expr("login_attempts + ?", 1)).Error; err != nil {
|
||||
log.Errorf("user: %s (update login attempts)", err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if err := Db().Model(m).Updates(map[string]interface{}{"login_attempts": 0, "login_at": TimeStamp()}).Error; err != nil {
|
||||
log.Errorf("user: %s (update last login)", err)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Role returns the user role for ACL permission checks.
|
||||
func (m *User) Role() acl.Role {
|
||||
if m.RoleAdmin {
|
||||
return acl.RoleAdmin
|
||||
}
|
||||
|
||||
if m.RoleChild {
|
||||
return acl.RoleChild
|
||||
}
|
||||
|
||||
if m.RoleFamily {
|
||||
return acl.RoleFamily
|
||||
}
|
||||
|
||||
if m.RoleFriend {
|
||||
return acl.RoleFriend
|
||||
}
|
||||
|
||||
if m.RoleGuest {
|
||||
return acl.RoleGuest
|
||||
}
|
||||
|
||||
return acl.RoleDefault
|
||||
}
|
||||
|
||||
// Validate Makes sure username and email are unique and meet requirements. Returns error if any property is invalid
|
||||
func (m *User) Validate() error {
|
||||
if m.Username() == "" {
|
||||
return errors.New("username must not be empty")
|
||||
}
|
||||
|
||||
if len(m.Username()) < UsernameLength {
|
||||
return fmt.Errorf("username must have at least %d characters", UsernameLength)
|
||||
}
|
||||
|
||||
var err error
|
||||
var resultName = User{}
|
||||
|
||||
if err = Db().Where("user_name = ? AND id <> ?", m.Username(), m.ID).First(&resultName).Error; err == nil {
|
||||
return errors.New("username already exists")
|
||||
} else if err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
// stop here if no email is provided
|
||||
if m.PrimaryEmail == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validate email address
|
||||
if a, err := mail.ParseAddress(m.PrimaryEmail); err != nil {
|
||||
return err
|
||||
} else {
|
||||
m.PrimaryEmail = a.Address // make sure email address will be used without name
|
||||
}
|
||||
|
||||
var resultMail = User{}
|
||||
|
||||
if err = Db().Where("primary_email = ? AND id <> ?", m.PrimaryEmail, m.ID).First(&resultMail).Error; err == nil {
|
||||
return errors.New("email already exists")
|
||||
} else if err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateWithPassword Creates User with Password in db transaction.
|
||||
func CreateWithPassword(uc form.UserCreate) error {
|
||||
u := &User{
|
||||
FullName: uc.FullName,
|
||||
UserName: uc.UserName,
|
||||
PrimaryEmail: uc.Email,
|
||||
RoleAdmin: true,
|
||||
}
|
||||
|
||||
if len(uc.Password) < PasswordLength {
|
||||
return fmt.Errorf("password must have at least %d characters", PasswordLength)
|
||||
}
|
||||
|
||||
err := u.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return Db().Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(u).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
pw := NewPassword(u.UserUID, uc.Password)
|
||||
if err := tx.Create(&pw).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("created user %s with uid %s", clean.Log(u.Username()), clean.Log(u.UserUID))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package entity
|
||||
|
||||
type UserMap map[string]User
|
||||
|
||||
func (m UserMap) Get(name string) User {
|
||||
if result, ok := m[name]; ok {
|
||||
return result
|
||||
}
|
||||
|
||||
return User{}
|
||||
}
|
||||
|
||||
func (m UserMap) Pointer(name string) *User {
|
||||
if result, ok := m[name]; ok {
|
||||
return &result
|
||||
}
|
||||
|
||||
return &User{}
|
||||
}
|
||||
|
||||
var UserFixtures = UserMap{
|
||||
"alice": {
|
||||
ID: 5,
|
||||
AddressID: 1,
|
||||
UserUID: "uqxetse3cy5eo9z2",
|
||||
UserName: "alice",
|
||||
FullName: "Alice",
|
||||
RoleAdmin: true,
|
||||
RoleGuest: false,
|
||||
UserDisabled: false,
|
||||
PrimaryEmail: "alice@example.com",
|
||||
},
|
||||
"bob": {
|
||||
ID: 7,
|
||||
AddressID: 1,
|
||||
UserUID: "uqxc08w3d0ej2283",
|
||||
UserName: "bob",
|
||||
FullName: "Bob",
|
||||
RoleAdmin: false,
|
||||
RoleGuest: false,
|
||||
UserDisabled: false,
|
||||
PrimaryEmail: "bob@example.com",
|
||||
},
|
||||
"friend": {
|
||||
ID: 8,
|
||||
AddressID: 1,
|
||||
UserUID: "uqxqg7i1kperxvu7",
|
||||
UserName: "friend",
|
||||
FullName: "Guy Friend",
|
||||
RoleAdmin: false,
|
||||
RoleGuest: false,
|
||||
RoleFriend: true,
|
||||
UserDisabled: true,
|
||||
PrimaryEmail: "friend@example.com",
|
||||
},
|
||||
"deleted": {
|
||||
ID: 10000008,
|
||||
AddressID: 1000000,
|
||||
UserUID: "uqxqg7i1kperxvu8",
|
||||
UserName: "deleted",
|
||||
FullName: "Deleted User",
|
||||
RoleAdmin: false,
|
||||
RoleGuest: true,
|
||||
RoleFriend: false,
|
||||
UserDisabled: false,
|
||||
PrimaryEmail: "",
|
||||
DeletedAt: &deleteTime,
|
||||
},
|
||||
}
|
||||
|
||||
// CreateUserFixtures inserts known entities into the database for testing.
|
||||
func CreateUserFixtures() {
|
||||
for _, entity := range UserFixtures {
|
||||
Db().Create(&entity)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package form
|
||||
|
||||
type Login struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
UserName string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
@@ -11,8 +11,8 @@ func (f Login) HasToken() bool {
|
||||
return f.Token != ""
|
||||
}
|
||||
|
||||
func (f Login) HasUserName() bool {
|
||||
return f.UserName != "" && len(f.UserName) <= 255
|
||||
func (f Login) HasUsername() bool {
|
||||
return f.Username != "" && len(f.Username) <= 255
|
||||
}
|
||||
|
||||
func (f Login) HasPassword() bool {
|
||||
@@ -20,5 +20,5 @@ func (f Login) HasPassword() bool {
|
||||
}
|
||||
|
||||
func (f Login) HasCredentials() bool {
|
||||
return f.HasUserName() && f.HasPassword()
|
||||
return f.HasUsername() && f.HasPassword()
|
||||
}
|
||||
|
||||
@@ -8,23 +8,23 @@ import (
|
||||
|
||||
func TestLogin_HasToken(t *testing.T) {
|
||||
t.Run("false", func(t *testing.T) {
|
||||
form := &Login{Email: "test@test.com", UserName: "John", Password: "passwd", Token: ""}
|
||||
form := &Login{Email: "test@test.com", Username: "John", Password: "passwd", Token: ""}
|
||||
assert.Equal(t, false, form.HasToken())
|
||||
})
|
||||
t.Run("true", func(t *testing.T) {
|
||||
form := &Login{Email: "test@test.com", UserName: "John", Password: "passwd", Token: "123"}
|
||||
form := &Login{Email: "test@test.com", Username: "John", Password: "passwd", Token: "123"}
|
||||
assert.Equal(t, true, form.HasToken())
|
||||
})
|
||||
}
|
||||
|
||||
func TestLogin_HasUserName(t *testing.T) {
|
||||
func TestLogin_HasUsername(t *testing.T) {
|
||||
t.Run("false", func(t *testing.T) {
|
||||
form := &Login{Email: "test@test.com", Password: "passwd", Token: ""}
|
||||
assert.Equal(t, false, form.HasUserName())
|
||||
assert.Equal(t, false, form.HasUsername())
|
||||
})
|
||||
t.Run("true", func(t *testing.T) {
|
||||
form := &Login{Email: "test@test.com", UserName: "John", Password: "passwd", Token: "123"}
|
||||
assert.Equal(t, true, form.HasUserName())
|
||||
form := &Login{Email: "test@test.com", Username: "John", Password: "passwd", Token: "123"}
|
||||
assert.Equal(t, true, form.HasUsername())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestLogin_HasPassword(t *testing.T) {
|
||||
assert.Equal(t, false, form.HasPassword())
|
||||
})
|
||||
t.Run("true", func(t *testing.T) {
|
||||
form := &Login{Email: "test@test.com", UserName: "John", Password: "passwd", Token: "123"}
|
||||
form := &Login{Email: "test@test.com", Username: "John", Password: "passwd", Token: "123"}
|
||||
assert.Equal(t, true, form.HasPassword())
|
||||
})
|
||||
}
|
||||
@@ -45,7 +45,7 @@ func TestLogin_HasCredentials(t *testing.T) {
|
||||
assert.Equal(t, false, form.HasCredentials())
|
||||
})
|
||||
t.Run("true", func(t *testing.T) {
|
||||
form := &Login{Email: "test@test.com", UserName: "John", Password: "passwd", Token: "123"}
|
||||
form := &Login{Email: "test@test.com", Username: "John", Password: "passwd", Token: "123"}
|
||||
assert.Equal(t, true, form.HasCredentials())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@ import "github.com/photoprism/photoprism/pkg/clean"
|
||||
|
||||
// UserCreate represents a User with a new password.
|
||||
type UserCreate struct {
|
||||
UserName string `json:"username"`
|
||||
FullName string `json:"fullname"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// Username returns the normalized username in lowercase and without whitespace padding.
|
||||
func (f UserCreate) Username() string {
|
||||
return clean.Username(f.UserName)
|
||||
// UsernameClean returns the username in lowercase and with whitespace trimmed.
|
||||
func (f UserCreate) UsernameClean() string {
|
||||
return clean.Login(f.Username)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ var DialectMySQL = Migrations{
|
||||
{
|
||||
ID: "20220329-060000",
|
||||
Dialect: "mysql",
|
||||
Statements: []string{"ALTER TABLE accounts MODIFY acc_url VARCHAR(255);", "ALTER TABLE addresses MODIFY address_notes VARCHAR(1024);", "ALTER TABLE albums MODIFY album_caption VARCHAR(1024);", "ALTER TABLE albums MODIFY album_description VARCHAR(2048);", "ALTER TABLE albums MODIFY album_notes VARCHAR(1024);", "ALTER TABLE cameras MODIFY camera_description VARCHAR(2048);", "ALTER TABLE cameras MODIFY camera_notes VARCHAR(1024);", "ALTER TABLE countries MODIFY country_description VARCHAR(2048);", "ALTER TABLE countries MODIFY country_notes VARCHAR(1024);", "ALTER TABLE details MODIFY keywords VARCHAR(2048);", "ALTER TABLE details MODIFY notes VARCHAR(2048);", "ALTER TABLE details MODIFY subject VARCHAR(1024);", "ALTER TABLE details MODIFY artist VARCHAR(1024);", "ALTER TABLE details MODIFY copyright VARCHAR(1024);", "ALTER TABLE details MODIFY license VARCHAR(1024);", "ALTER TABLE folders MODIFY folder_description VARCHAR(2048);", "ALTER TABLE labels MODIFY label_description VARCHAR(2048);", "ALTER TABLE labels MODIFY label_notes VARCHAR(1024);", "ALTER TABLE lenses MODIFY lens_description VARCHAR(2048);", "ALTER TABLE lenses MODIFY lens_notes VARCHAR(1024);", "ALTER TABLE subjects MODIFY subj_bio VARCHAR(2048);", "ALTER TABLE subjects MODIFY subj_notes VARCHAR(1024);"},
|
||||
Statements: []string{"ALTER TABLE accounts MODIFY acc_url VARCHAR(255);", "ALTER TABLE albums MODIFY album_caption VARCHAR(1024);", "ALTER TABLE albums MODIFY album_description VARCHAR(2048);", "ALTER TABLE albums MODIFY album_notes VARCHAR(1024);", "ALTER TABLE cameras MODIFY camera_description VARCHAR(2048);", "ALTER TABLE cameras MODIFY camera_notes VARCHAR(1024);", "ALTER TABLE countries MODIFY country_description VARCHAR(2048);", "ALTER TABLE countries MODIFY country_notes VARCHAR(1024);", "ALTER TABLE details MODIFY keywords VARCHAR(2048);", "ALTER TABLE details MODIFY notes VARCHAR(2048);", "ALTER TABLE details MODIFY subject VARCHAR(1024);", "ALTER TABLE details MODIFY artist VARCHAR(1024);", "ALTER TABLE details MODIFY copyright VARCHAR(1024);", "ALTER TABLE details MODIFY license VARCHAR(1024);", "ALTER TABLE folders MODIFY folder_description VARCHAR(2048);", "ALTER TABLE labels MODIFY label_description VARCHAR(2048);", "ALTER TABLE labels MODIFY label_notes VARCHAR(1024);", "ALTER TABLE lenses MODIFY lens_description VARCHAR(2048);", "ALTER TABLE lenses MODIFY lens_notes VARCHAR(1024);", "ALTER TABLE subjects MODIFY subj_bio VARCHAR(2048);", "ALTER TABLE subjects MODIFY subj_notes VARCHAR(1024);"},
|
||||
},
|
||||
{
|
||||
ID: "20220329-061000",
|
||||
@@ -98,4 +98,9 @@ var DialectMySQL = Migrations{
|
||||
Dialect: "mysql",
|
||||
Statements: []string{"ALTER TABLE files MODIFY file_chroma SMALLINT DEFAULT -1;"},
|
||||
},
|
||||
{
|
||||
ID: "20220901-000100",
|
||||
Dialect: "mysql",
|
||||
Statements: []string{"INSERT IGNORE INTO auth_users (id, user_uid, super_admin, user_role, display_name, user_slug, username, email, login_attempts, login_at, created_at, updated_at) SELECT id, user_uid, role_admin, 'admin', full_name, user_name, user_name, primary_email, login_attempts, login_at, created_at, updated_at FROM users WHERE user_name <> '' AND user_name IS NOT NULL AND role_admin = 1 AND user_disabled = 0;"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -58,4 +58,9 @@ var DialectSQLite3 = Migrations{
|
||||
Dialect: "sqlite3",
|
||||
Statements: []string{"CREATE INDEX IF NOT EXISTS idx_files_missing_root ON files (file_missing, file_root);"},
|
||||
},
|
||||
{
|
||||
ID: "20220901-000100",
|
||||
Dialect: "sqlite3",
|
||||
Statements: []string{"INSERT OR IGNORE INTO auth_users (id, user_uid, super_admin, user_role, display_name, user_slug, username, email, login_attempts, login_at, created_at, updated_at) SELECT id, user_uid, 1, 'admin', full_name, user_name, user_name, primary_email, login_attempts, login_at, created_at, updated_at FROM users WHERE user_name <> '' AND user_name IS NOT NULL AND user_uid <> '' AND user_uid IS NOT NULL AND role_admin = 1 AND user_disabled = 0 ON CONFLICT DO NOTHING;"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -110,7 +110,10 @@ func (m *Migration) Execute(db *gorm.DB) error {
|
||||
log.Tracef("migrate: %s (ignored, probably didn't exist anymore)", err)
|
||||
} else if strings.HasPrefix(q, "DROP TABLE ") &&
|
||||
strings.Contains(e, "DROP") {
|
||||
log.Tracef("migrate: %s (ignored, probably didn't exist anymored)", err)
|
||||
log.Tracef("migrate: %s (ignored, probably didn't exist anymore)", err)
|
||||
} else if strings.Contains(q, " IGNORE ") &&
|
||||
(strings.Contains(e, "NO SUCH TABLE") || strings.Contains(e, "DOESN'T EXIST")) {
|
||||
log.Tracef("migrate: %s (ignored, old table does not exist anymore)", err)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
ALTER TABLE accounts MODIFY acc_url VARCHAR(255);
|
||||
ALTER TABLE addresses MODIFY address_notes VARCHAR(1024);
|
||||
ALTER TABLE albums MODIFY album_caption VARCHAR(1024);
|
||||
ALTER TABLE albums MODIFY album_description VARCHAR(2048);
|
||||
ALTER TABLE albums MODIFY album_notes VARCHAR(1024);
|
||||
|
||||
1
internal/migrate/mysql/20220901-000100.sql
Normal file
1
internal/migrate/mysql/20220901-000100.sql
Normal file
@@ -0,0 +1 @@
|
||||
INSERT IGNORE INTO auth_users (id, user_uid, super_admin, user_role, display_name, user_slug, username, email, login_attempts, login_at, created_at, updated_at) SELECT id, user_uid, role_admin, 'admin', full_name, user_name, user_name, primary_email, login_attempts, login_at, created_at, updated_at FROM users WHERE user_name <> '' AND user_name IS NOT NULL AND role_admin = 1 AND user_disabled = 0;
|
||||
1
internal/migrate/sqlite3/20220901-000100.sql
Normal file
1
internal/migrate/sqlite3/20220901-000100.sql
Normal file
@@ -0,0 +1 @@
|
||||
INSERT OR IGNORE INTO auth_users (id, user_uid, super_admin, user_role, display_name, user_slug, username, email, login_attempts, login_at, created_at, updated_at) SELECT id, user_uid, 1, 'admin', full_name, user_name, user_name, primary_email, login_attempts, login_at, created_at, updated_at FROM users WHERE user_name <> '' AND user_name IS NOT NULL AND user_uid <> '' AND user_uid IS NOT NULL AND role_admin = 1 AND user_disabled = 0 ON CONFLICT DO NOTHING;
|
||||
@@ -11,7 +11,7 @@ func TestRegisteredUsers(t *testing.T) {
|
||||
users := RegisteredUsers()
|
||||
|
||||
for _, user := range users {
|
||||
t.Logf("user: %v, %s, %s, %s", user.ID, user.UserUID, user.Username(), user.FullName)
|
||||
t.Logf("user: %v, %s, %s, %s", user.ID, user.UserUID, user.UserName(), user.DisplayName)
|
||||
}
|
||||
|
||||
t.Logf("user count: %v", len(users))
|
||||
|
||||
@@ -57,7 +57,7 @@ func BasicAuth() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
user := entity.FindUserByName(username)
|
||||
user := entity.FindUserByLogin(username)
|
||||
|
||||
if user != nil {
|
||||
invalid = user.InvalidPassword(password)
|
||||
|
||||
@@ -44,7 +44,7 @@ func (s Data) Valid() bool {
|
||||
}
|
||||
|
||||
func (s Data) Guest() bool {
|
||||
return s.User.Guest()
|
||||
return s.User.IsGuest()
|
||||
}
|
||||
|
||||
func (s Data) NoShares() bool {
|
||||
|
||||
Reference in New Issue
Block a user