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,23 +131,36 @@ export default class Session {
|
|||||||
|
|
||||||
getEmail() {
|
getEmail() {
|
||||||
if (this.isUser()) {
|
if (this.isUser()) {
|
||||||
return this.user.PrimaryEmail;
|
return this.user.Email;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
getNickName() {
|
getUsername() {
|
||||||
if (this.isUser()) {
|
if (this.isUser()) {
|
||||||
return this.user.NickName;
|
if (this.user.Username) {
|
||||||
|
return this.user.Username;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
getFullName() {
|
getDisplayName() {
|
||||||
if (this.isUser()) {
|
if (this.isUser()) {
|
||||||
return this.user.FullName;
|
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 "";
|
return "";
|
||||||
@@ -158,7 +171,7 @@ export default class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
return this.user && this.user.hasId() && this.user.RoleAdmin;
|
return this.user && this.user.hasId() && (this.user.Role === "admin" || this.user.SuperAdmin);
|
||||||
}
|
}
|
||||||
|
|
||||||
isAnonymous() {
|
isAnonymous() {
|
||||||
|
|||||||
@@ -631,11 +631,11 @@ export default {
|
|||||||
},
|
},
|
||||||
displayName() {
|
displayName() {
|
||||||
const user = this.$session.getUser();
|
const user = this.$session.getUser();
|
||||||
return user.FullName ? user.FullName : user.UserName;
|
return user.DisplayName ? user.DisplayName : user.Username;
|
||||||
},
|
},
|
||||||
accountInfo() {
|
accountInfo() {
|
||||||
const user = this.$session.getUser();
|
const user = this.$session.getUser();
|
||||||
return user.PrimaryEmail ? user.PrimaryEmail : this.$gettext("Account");
|
return user.Email ? user.Email : this.$gettext("Account");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|||||||
@@ -32,56 +32,72 @@ export class User extends RestModel {
|
|||||||
getDefaults() {
|
getDefaults() {
|
||||||
return {
|
return {
|
||||||
UID: "",
|
UID: "",
|
||||||
Address: {},
|
Slug: "",
|
||||||
MotherUID: "",
|
Username: "",
|
||||||
FatherUID: "",
|
Email: "",
|
||||||
GlobalUID: "",
|
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: "",
|
FullName: "",
|
||||||
NickName: "",
|
Alias: "",
|
||||||
MaidenName: "",
|
|
||||||
ArtistName: "",
|
ArtistName: "",
|
||||||
UserName: "",
|
Artist: false,
|
||||||
UserStatus: "",
|
Favorite: false,
|
||||||
UserDisabled: false,
|
Hidden: false,
|
||||||
UserSettings: "",
|
Private: false,
|
||||||
PrimaryEmail: "",
|
Excluded: false,
|
||||||
EmailConfirmed: false,
|
|
||||||
BackupEmail: "",
|
|
||||||
PersonURL: "",
|
|
||||||
PersonPhone: "",
|
|
||||||
PersonStatus: "",
|
|
||||||
PersonAvatar: "",
|
|
||||||
PersonLocation: "",
|
|
||||||
PersonBio: "",
|
|
||||||
BusinessURL: "",
|
|
||||||
BusinessPhone: "",
|
|
||||||
BusinessEmail: "",
|
|
||||||
CompanyName: "",
|
CompanyName: "",
|
||||||
DepartmentName: "",
|
DepartmentName: "",
|
||||||
JobTitle: "",
|
JobTitle: "",
|
||||||
|
BusinessURL: "",
|
||||||
|
BusinessPhone: "",
|
||||||
|
BusinessEmail: "",
|
||||||
|
BackupEmail: "",
|
||||||
BirthYear: -1,
|
BirthYear: -1,
|
||||||
BirthMonth: -1,
|
BirthMonth: -1,
|
||||||
BirthDay: -1,
|
BirthDay: -1,
|
||||||
TermsAccepted: false,
|
FileRoot: "",
|
||||||
IsArtist: false,
|
FilePath: "",
|
||||||
IsSubject: false,
|
|
||||||
RoleAdmin: false,
|
|
||||||
RoleGuest: false,
|
|
||||||
RoleChild: false,
|
|
||||||
RoleFamily: false,
|
|
||||||
RoleFriend: false,
|
|
||||||
WebDAV: false,
|
|
||||||
StoragePath: "",
|
|
||||||
CanInvite: false,
|
|
||||||
InviteToken: "",
|
InviteToken: "",
|
||||||
InvitedBy: "",
|
InvitedBy: "",
|
||||||
|
DownloadToken: "",
|
||||||
|
PreviewToken: "",
|
||||||
|
ConfirmedAt: "",
|
||||||
|
TermsAccepted: "",
|
||||||
CreatedAt: "",
|
CreatedAt: "",
|
||||||
UpdatedAt: "",
|
UpdatedAt: "",
|
||||||
|
DeletedAt: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getEntityName() {
|
getEntityName() {
|
||||||
return this.FullName ? this.FullName : this.UserName;
|
if (this.DisplayName) {
|
||||||
|
return this.DisplayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.FullName) {
|
||||||
|
return this.FullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRegisterForm() {
|
getRegisterForm() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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-layout align-center justify-center>
|
||||||
<v-flex xs12 sm8 md4 xl3 xxl2>
|
<v-flex xs12 sm8 md4 xl3 xxl2>
|
||||||
<v-form ref="form" dense class="auth-login-form" accept-charset="UTF-8" @submit.prevent="login">
|
<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 = {
|
const values = {
|
||||||
user: {
|
user: {
|
||||||
ID: 5,
|
ID: 5,
|
||||||
NickName: "Foo",
|
DisplayName: "Foo",
|
||||||
FullName: "Max Last",
|
FullName: "Max Last",
|
||||||
PrimaryEmail: "test@test.com",
|
Email: "test@test.com",
|
||||||
RoleAdmin: true,
|
Role: "admin",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
session.setData(values);
|
session.setData(values);
|
||||||
@@ -72,10 +72,10 @@ describe("common/session", () => {
|
|||||||
assert.equal(result, "test@test.com");
|
assert.equal(result, "test@test.com");
|
||||||
const values2 = {
|
const values2 = {
|
||||||
user: {
|
user: {
|
||||||
NickName: "Foo",
|
DisplayName: "Foo",
|
||||||
FullName: "Max Last",
|
FullName: "Max Last",
|
||||||
PrimaryEmail: "test@test.com",
|
Email: "test@test.com",
|
||||||
RoleAdmin: true,
|
Role: "admin",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
session.setData(values2);
|
session.setData(values2);
|
||||||
@@ -90,25 +90,25 @@ describe("common/session", () => {
|
|||||||
const values = {
|
const values = {
|
||||||
user: {
|
user: {
|
||||||
ID: 5,
|
ID: 5,
|
||||||
NickName: "Foo",
|
DisplayName: "Foo",
|
||||||
FullName: "Max Last",
|
FullName: "Max Last",
|
||||||
PrimaryEmail: "test@test.com",
|
Email: "test@test.com",
|
||||||
RoleAdmin: true,
|
Role: "admin",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
session.setData(values);
|
session.setData(values);
|
||||||
const result = session.getNickName();
|
const result = session.getDisplayName();
|
||||||
assert.equal(result, "Foo");
|
assert.equal(result, "Foo");
|
||||||
const values2 = {
|
const values2 = {
|
||||||
user: {
|
user: {
|
||||||
NickName: "Bar",
|
DisplayName: "Bar",
|
||||||
FullName: "Max Last",
|
FullName: "Max Last",
|
||||||
PrimaryEmail: "test@test.com",
|
Email: "test@test.com",
|
||||||
RoleAdmin: true,
|
Role: "admin",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
session.setData(values2);
|
session.setData(values2);
|
||||||
const result2 = session.getNickName();
|
const result2 = session.getDisplayName();
|
||||||
assert.equal(result2, "");
|
assert.equal(result2, "");
|
||||||
session.deleteData();
|
session.deleteData();
|
||||||
});
|
});
|
||||||
@@ -119,25 +119,25 @@ describe("common/session", () => {
|
|||||||
const values = {
|
const values = {
|
||||||
user: {
|
user: {
|
||||||
ID: 5,
|
ID: 5,
|
||||||
NickName: "Foo",
|
DisplayName: "Foo",
|
||||||
FullName: "Max Last",
|
FullName: "Max Last",
|
||||||
PrimaryEmail: "test@test.com",
|
Email: "test@test.com",
|
||||||
RoleAdmin: true,
|
Role: "admin",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
session.setData(values);
|
session.setData(values);
|
||||||
const result = session.getFullName();
|
const result = session.getDisplayName();
|
||||||
assert.equal(result, "Max Last");
|
assert.equal(result, "Foo");
|
||||||
const values2 = {
|
const values2 = {
|
||||||
user: {
|
user: {
|
||||||
NickName: "Bar",
|
DisplayName: "Bar",
|
||||||
FullName: "Max New",
|
FullName: "Max New",
|
||||||
PrimaryEmail: "test@test.com",
|
Email: "test@test.com",
|
||||||
RoleAdmin: true,
|
Role: "admin",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
session.setData(values2);
|
session.setData(values2);
|
||||||
const result2 = session.getFullName();
|
const result2 = session.getDisplayName();
|
||||||
assert.equal(result2, "");
|
assert.equal(result2, "");
|
||||||
session.deleteData();
|
session.deleteData();
|
||||||
});
|
});
|
||||||
@@ -148,10 +148,10 @@ describe("common/session", () => {
|
|||||||
const values = {
|
const values = {
|
||||||
user: {
|
user: {
|
||||||
ID: 5,
|
ID: 5,
|
||||||
NickName: "Foo",
|
DisplayName: "Foo",
|
||||||
FullName: "Max Last",
|
FullName: "Max Last",
|
||||||
PrimaryEmail: "test@test.com",
|
Email: "test@test.com",
|
||||||
RoleAdmin: true,
|
Role: "admin",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
session.setData(values);
|
session.setData(values);
|
||||||
@@ -166,10 +166,10 @@ describe("common/session", () => {
|
|||||||
const values = {
|
const values = {
|
||||||
user: {
|
user: {
|
||||||
ID: 5,
|
ID: 5,
|
||||||
NickName: "Foo",
|
DisplayName: "Foo",
|
||||||
FullName: "Max Last",
|
FullName: "Max Last",
|
||||||
PrimaryEmail: "test@test.com",
|
Email: "test@test.com",
|
||||||
RoleAdmin: true,
|
Role: "admin",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
session.setData(values);
|
session.setData(values);
|
||||||
@@ -184,10 +184,10 @@ describe("common/session", () => {
|
|||||||
const values = {
|
const values = {
|
||||||
user: {
|
user: {
|
||||||
ID: 5,
|
ID: 5,
|
||||||
NickName: "Foo",
|
DisplayName: "Foo",
|
||||||
FullName: "Max Last",
|
FullName: "Max Last",
|
||||||
PrimaryEmail: "test@test.com",
|
Email: "test@test.com",
|
||||||
RoleAdmin: true,
|
Role: "admin",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
session.setData(values);
|
session.setData(values);
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ let assert = chai.assert;
|
|||||||
|
|
||||||
describe("model/user", () => {
|
describe("model/user", () => {
|
||||||
it("should get entity name", () => {
|
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 user = new User(values);
|
||||||
const result = user.getEntityName();
|
const result = user.getEntityName();
|
||||||
assert.equal(result, "Max Last");
|
assert.equal(result, "Max Last");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get id", () => {
|
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 user = new User(values);
|
||||||
const result = user.getId();
|
const result = user.getId();
|
||||||
assert.equal(result, 5);
|
assert.equal(result, 5);
|
||||||
@@ -30,28 +30,28 @@ describe("model/user", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should get register form", async () => {
|
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 user = new User(values);
|
||||||
const result = await user.getRegisterForm();
|
const result = await user.getRegisterForm();
|
||||||
assert.equal(result.definition.foo, "register");
|
assert.equal(result.definition.foo, "register");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get profile form", async () => {
|
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 user = new User(values);
|
||||||
const result = await user.getProfileForm();
|
const result = await user.getProfileForm();
|
||||||
assert.equal(result.definition.foo, "profile");
|
assert.equal(result.definition.foo, "profile");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get change password", async () => {
|
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 user = new User(values);
|
||||||
const result = await user.changePassword("old", "new");
|
const result = await user.changePassword("old", "new");
|
||||||
assert.equal(result.new_password, "new");
|
assert.equal(result.new_password, "new");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should save profile", async () => {
|
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);
|
const user = new User(values);
|
||||||
assert.equal(user.FullName, "Max Last");
|
assert.equal(user.FullName, "Max Last");
|
||||||
await user.saveProfile();
|
await user.saveProfile();
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/form"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
|
||||||
"github.com/sirupsen/logrus"
|
"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.
|
// 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) {
|
func AuthenticateUser(app *gin.Engine, router *gin.RouterGroup, username string, password string) (sessId string) {
|
||||||
CreateSession(router)
|
CreateSession(router)
|
||||||
f := form.Login{
|
f := form.Login{
|
||||||
UserName: username,
|
Username: username,
|
||||||
Password: password,
|
Password: password,
|
||||||
}
|
}
|
||||||
loginStr, err := json.Marshal(f)
|
loginStr, err := json.Marshal(f)
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
@@ -14,6 +12,7 @@ import (
|
|||||||
"github.com/photoprism/photoprism/internal/event"
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
"github.com/photoprism/photoprism/internal/i18n"
|
"github.com/photoprism/photoprism/internal/i18n"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,9 +30,9 @@ func GetConfig(router *gin.RouterGroup) {
|
|||||||
|
|
||||||
conf := service.Config()
|
conf := service.Config()
|
||||||
|
|
||||||
if s.User.Guest() {
|
if s.User.IsGuest() {
|
||||||
c.JSON(http.StatusOK, conf.GuestConfig())
|
c.JSON(http.StatusOK, conf.GuestConfig())
|
||||||
} else if s.User.Registered() {
|
} else if s.User.IsRegistered() {
|
||||||
c.JSON(http.StatusOK, conf.UserConfig())
|
c.JSON(http.StatusOK, conf.UserConfig())
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusOK, conf.PublicConfig())
|
c.JSON(http.StatusOK, conf.PublicConfig())
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func Connect(router *gin.RouterGroup) {
|
|||||||
s := Auth(SessionID(c), acl.ResourceConfigOptions, acl.ActionUpdate)
|
s := Auth(SessionID(c), acl.ResourceConfigOptions, acl.ActionUpdate)
|
||||||
|
|
||||||
if s.Invalid() {
|
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)
|
AbortUnauthorized(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ package api
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/acl"
|
"github.com/photoprism/photoprism/internal/acl"
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/photoprism/photoprism/internal/form"
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
"github.com/photoprism/photoprism/internal/i18n"
|
"github.com/photoprism/photoprism/internal/i18n"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
"github.com/photoprism/photoprism/internal/session"
|
"github.com/photoprism/photoprism/internal/session"
|
||||||
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
)
|
)
|
||||||
|
|
||||||
// POST /api/v1/session
|
// POST /api/v1/session
|
||||||
@@ -52,11 +52,11 @@ func CreateSession(router *gin.RouterGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upgrade from anonymous to guest. Don't downgrade.
|
// Upgrade from anonymous to guest. Don't downgrade.
|
||||||
if data.User.Anonymous() {
|
if data.User.IsAnonymous() {
|
||||||
data.User = entity.Guest
|
data.User = entity.Guest
|
||||||
}
|
}
|
||||||
} else if f.HasCredentials() {
|
} else if f.HasCredentials() {
|
||||||
user := entity.FindUserByName(f.UserName)
|
user := entity.FindUserByLogin(f.Username)
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
c.AbortWithStatusJSON(400, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
c.AbortWithStatusJSON(400, gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||||
@@ -80,7 +80,7 @@ func CreateSession(router *gin.RouterGroup) {
|
|||||||
|
|
||||||
AddSessionHeader(c, id)
|
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()})
|
c.JSON(http.StatusOK, gin.H{"status": "ok", "id": id, "data": data, "config": conf.GuestConfig()})
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "id": id, "data": data, "config": conf.UserConfig()})
|
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 {
|
func SessionID(c *gin.Context) string {
|
||||||
return c.GetHeader("X-Session-ID")
|
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 {
|
func Auth(id string, resource acl.Resource, action acl.Action) session.Data {
|
||||||
sess := Session(id)
|
sess := Session(id)
|
||||||
|
|
||||||
if acl.Permissions.Deny(resource, sess.User.Role(), action) {
|
if acl.Permissions.Deny(resource, sess.User.AclRole(), action) {
|
||||||
return session.Data{}
|
return session.Data{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/i18n"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCreateSession(t *testing.T) {
|
func TestCreateSession(t *testing.T) {
|
||||||
@@ -14,7 +15,8 @@ func TestCreateSession(t *testing.T) {
|
|||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
CreateSession(router)
|
CreateSession(router)
|
||||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"username": "admin", "password": "photoprism"}`)
|
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, "admin", val2.String())
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
})
|
})
|
||||||
@@ -48,8 +50,8 @@ func TestCreateSession(t *testing.T) {
|
|||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
CreateSession(router)
|
CreateSession(router)
|
||||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"username": "alice", "password": "Alice123!"}`)
|
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"username": "alice", "password": "Alice123!"}`)
|
||||||
resEmail := gjson.Get(r.Body.String(), "data.user.PrimaryEmail")
|
resEmail := gjson.Get(r.Body.String(), "data.user.Email")
|
||||||
resUsername := gjson.Get(r.Body.String(), "data.user.UserName")
|
resUsername := gjson.Get(r.Body.String(), "data.user.Username")
|
||||||
assert.Equal(t, "alice@example.com", resEmail.String())
|
assert.Equal(t, "alice@example.com", resEmail.String())
|
||||||
assert.Equal(t, "alice", resUsername.String())
|
assert.Equal(t, "alice", resUsername.String())
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
@@ -58,8 +60,8 @@ func TestCreateSession(t *testing.T) {
|
|||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
CreateSession(router)
|
CreateSession(router)
|
||||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"username": "bob", "password": "Bobbob123!"}`)
|
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/session", `{"username": "bob", "password": "Bobbob123!"}`)
|
||||||
resEmail := gjson.Get(r.Body.String(), "data.user.PrimaryEmail")
|
resEmail := gjson.Get(r.Body.String(), "data.user.Email")
|
||||||
resUsername := gjson.Get(r.Body.String(), "data.user.UserName")
|
resUsername := gjson.Get(r.Body.String(), "data.user.Username")
|
||||||
assert.Equal(t, "bob@example.com", resEmail.String())
|
assert.Equal(t, "bob@example.com", resEmail.String())
|
||||||
assert.Equal(t, "bob", resUsername.String())
|
assert.Equal(t, "bob", resUsername.String())
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
"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
|
var clientConfig config.ClientConfig
|
||||||
|
|
||||||
if sess.User.Guest() {
|
if sess.User.IsGuest() {
|
||||||
clientConfig = conf.GuestConfig()
|
clientConfig = conf.GuestConfig()
|
||||||
} else if sess.User.Registered() {
|
} else if sess.User.IsRegistered() {
|
||||||
clientConfig = conf.UserConfig()
|
clientConfig = conf.UserConfig()
|
||||||
} else {
|
} else {
|
||||||
clientConfig = conf.PublicConfig()
|
clientConfig = conf.PublicConfig()
|
||||||
@@ -150,7 +151,7 @@ func wsWriter(ws *websocket.Conn, writeMutex *sync.Mutex, connId string) {
|
|||||||
|
|
||||||
wsAuth.mutex.RUnlock()
|
wsAuth.mutex.RUnlock()
|
||||||
|
|
||||||
if user.Registered() {
|
if user.IsRegistered() {
|
||||||
writeMutex.Lock()
|
writeMutex.Lock()
|
||||||
|
|
||||||
if err := ws.SetWriteDeadline(time.Now().Add(30 * time.Second)); err != nil {
|
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"
|
"time"
|
||||||
|
|
||||||
"github.com/leandro-lugaresi/hub"
|
"github.com/leandro-lugaresi/hub"
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
"github.com/photoprism/photoprism/pkg/capture"
|
"github.com/photoprism/photoprism/pkg/capture"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,13 +19,14 @@ func TestIndexCommand(t *testing.T) {
|
|||||||
|
|
||||||
s := event.Subscribe("log.info")
|
s := event.Subscribe("log.info")
|
||||||
defer event.Unsubscribe(s)
|
defer event.Unsubscribe(s)
|
||||||
logs := ""
|
|
||||||
|
var l string
|
||||||
|
|
||||||
assert.IsType(t, hub.Subscription{}, s)
|
assert.IsType(t, hub.Subscription{}, s)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for msg := range s.Receiver {
|
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 != "" {
|
if stdout != "" {
|
||||||
t.Errorf("unexpected stdout output: %s", stdout)
|
t.Logf("stdout: %s", stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
if output := logs; output != "" {
|
if l != "" {
|
||||||
// Expected index command output.
|
// Expected index command output.
|
||||||
assert.Contains(t, output, "indexing originals")
|
assert.Contains(t, l, "classify: loading labels")
|
||||||
assert.Contains(t, output, "indexed")
|
assert.NotContains(t, l, "error")
|
||||||
assert.Contains(t, output, "files")
|
assert.Contains(t, l, "found no .ppignore file")
|
||||||
} else {
|
} else {
|
||||||
t.Fatal("log output missing")
|
t.Fatal("log output missing")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func passwdAction(ctx *cli.Context) error {
|
|||||||
|
|
||||||
user := entity.Admin
|
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: ")
|
newPassword := getPassword("New Password: ")
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ func passwdAction(ctx *cli.Context) error {
|
|||||||
return err
|
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()
|
conf.Shutdown()
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"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.
|
// 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.
|
// resetIndexDb resets the index database schema.
|
||||||
func resetIndexDb(conf *config.Config) {
|
func resetIndexDb(c *config.Config) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
tables := entity.Entities
|
tables := entity.Entities
|
||||||
|
|
||||||
log.Infoln("dropping existing tables")
|
log.Infoln("dropping existing tables")
|
||||||
tables.Drop(conf.Db())
|
tables.Drop(c.Db())
|
||||||
|
|
||||||
log.Infoln("restoring default schema")
|
log.Infoln("restoring default schema")
|
||||||
entity.InitDb(true, false, nil)
|
entity.InitDb(true, false, nil)
|
||||||
|
|
||||||
if conf.AdminPassword() != "" {
|
// Reset admin account?
|
||||||
log.Infoln("restoring initial admin password")
|
if c.AdminPassword() == "" {
|
||||||
entity.Admin.InitPassword(conf.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))
|
log.Infof("database reset completed in %s", time.Since(start))
|
||||||
}
|
}
|
||||||
|
|
||||||
// resetCache removes all cache files and folders.
|
// resetCache removes all cache files and folders.
|
||||||
func resetCache(conf *config.Config) {
|
func resetCache(c *config.Config) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
matches, err := filepath.Glob(regexp.QuoteMeta(conf.CachePath()) + "/**")
|
matches, err := filepath.Glob(regexp.QuoteMeta(c.CachePath()) + "/**")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("reset: %s (find cache files)", err)
|
log.Errorf("reset: %s (find cache files)", err)
|
||||||
@@ -216,10 +219,10 @@ func resetCache(conf *config.Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resetSidecarJson removes generated *.json sidecar files.
|
// resetSidecarJson removes generated *.json sidecar files.
|
||||||
func resetSidecarJson(conf *config.Config) {
|
func resetSidecarJson(c *config.Config) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
matches, err := filepath.Glob(regexp.QuoteMeta(conf.SidecarPath()) + "/**/*.json")
|
matches, err := filepath.Glob(regexp.QuoteMeta(c.SidecarPath()) + "/**/*.json")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("reset: %s (find *.json sidecar files)", err)
|
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))
|
log.Infof("removing %d *.json sidecar files", len(matches))
|
||||||
|
|
||||||
for _, name := range matches {
|
for _, name := range matches {
|
||||||
if err := os.Remove(name); err != nil {
|
if err = os.Remove(name); err != nil {
|
||||||
fmt.Print("E")
|
fmt.Print("E")
|
||||||
} else {
|
} else {
|
||||||
fmt.Print(".")
|
fmt.Print(".")
|
||||||
@@ -246,10 +249,10 @@ func resetSidecarJson(conf *config.Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resetSidecarYaml removes generated *.yml files.
|
// resetSidecarYaml removes generated *.yml files.
|
||||||
func resetSidecarYaml(conf *config.Config) {
|
func resetSidecarYaml(c *config.Config) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
matches, err := filepath.Glob(regexp.QuoteMeta(conf.SidecarPath()) + "/**/*.yml")
|
matches, err := filepath.Glob(regexp.QuoteMeta(c.SidecarPath()) + "/**/*.yml")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("reset: %s (find *.yml metadata files)", err)
|
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/form"
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"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.
|
// UsersCommand registers user management subcommands.
|
||||||
var UsersCommand = cli.Command{
|
var UsersCommand = cli.Command{
|
||||||
Name: "users",
|
Name: "users",
|
||||||
Usage: "User management subcommands",
|
Usage: "User management subcommands",
|
||||||
Subcommands: []cli.Command{
|
Subcommands: []cli.Command{
|
||||||
{
|
{
|
||||||
Name: "list",
|
Name: "ls",
|
||||||
Usage: "Lists registered users",
|
Aliases: []string{"list"},
|
||||||
Action: usersListAction,
|
Usage: "Shows registered users",
|
||||||
|
Flags: report.CliFlags,
|
||||||
|
Action: usersListAction,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "add",
|
Name: "add",
|
||||||
Usage: "Adds a new user",
|
Aliases: []string{"create"},
|
||||||
Action: usersAddAction,
|
Usage: "Adds a new user account",
|
||||||
|
Action: usersAddAction,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.StringFlag{
|
|
||||||
Name: "fullname, n",
|
|
||||||
Usage: "full name of the new user",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "username, u",
|
Name: "username, u",
|
||||||
Usage: "unique username",
|
Usage: UsernameUsage,
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "password, p",
|
|
||||||
Usage: "sets the users password",
|
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "email, m",
|
Name: "email, m",
|
||||||
Usage: "sets the users email",
|
Usage: EmailUsage,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "password, p",
|
||||||
|
Usage: PasswordUsage,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "update",
|
Name: "mod",
|
||||||
Usage: "Updates user information",
|
Aliases: []string{"update"},
|
||||||
Action: usersUpdateAction,
|
Usage: "Updates a user account",
|
||||||
|
Action: usersUpdateAction,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "fullname, n",
|
Name: "username, u",
|
||||||
Usage: "full name of the new user",
|
Usage: UsernameUsage,
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "password, p",
|
|
||||||
Usage: "sets the users password",
|
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "email, m",
|
Name: "email, m",
|
||||||
Usage: "sets the users email",
|
Usage: EmailUsage,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "password, p",
|
||||||
|
Usage: PasswordUsage,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "delete",
|
Name: "rm",
|
||||||
Usage: "Removes an existing user",
|
Aliases: []string{"delete"},
|
||||||
|
Usage: "Removes a user account",
|
||||||
Action: usersDeleteAction,
|
Action: usersDeleteAction,
|
||||||
ArgsUsage: "[username]",
|
ArgsUsage: "[username]",
|
||||||
},
|
},
|
||||||
@@ -82,49 +88,43 @@ func usersAddAction(ctx *cli.Context) error {
|
|||||||
return callWithDependencies(ctx, func(conf *config.Config) error {
|
return callWithDependencies(ctx, func(conf *config.Config) error {
|
||||||
|
|
||||||
uc := form.UserCreate{
|
uc := form.UserCreate{
|
||||||
UserName: strings.TrimSpace(ctx.String("username")),
|
Username: strings.TrimSpace(ctx.String("username")),
|
||||||
FullName: strings.TrimSpace(ctx.String("fullname")),
|
|
||||||
Email: strings.TrimSpace(ctx.String("email")),
|
Email: strings.TrimSpace(ctx.String("email")),
|
||||||
Password: strings.TrimSpace(ctx.String("password")),
|
Password: strings.TrimSpace(ctx.String("password")),
|
||||||
}
|
}
|
||||||
|
|
||||||
interactive := true
|
interactive := true
|
||||||
|
|
||||||
if uc.UserName != "" && uc.Password != "" {
|
if uc.Username != "" && uc.Password != "" {
|
||||||
log.Debugf("creating user in non-interactive mode")
|
log.Debugf("creating user in non-interactive mode")
|
||||||
interactive = false
|
interactive = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if interactive && uc.FullName == "" {
|
if interactive && uc.Username == "" {
|
||||||
prompt := promptui.Prompt{
|
|
||||||
Label: "Full Name",
|
|
||||||
}
|
|
||||||
res, err := prompt.Run()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
uc.FullName = strings.TrimSpace(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
if interactive && uc.UserName == "" {
|
|
||||||
prompt := promptui.Prompt{
|
prompt := promptui.Prompt{
|
||||||
Label: "Username",
|
Label: "Username",
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := prompt.Run()
|
res, err := prompt.Run()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
uc.UserName = strings.TrimSpace(res)
|
|
||||||
|
uc.Username = strings.TrimSpace(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
if interactive && uc.Email == "" {
|
if interactive && uc.Email == "" {
|
||||||
prompt := promptui.Prompt{
|
prompt := promptui.Prompt{
|
||||||
Label: "E-Mail",
|
Label: "Email",
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := prompt.Run()
|
res, err := prompt.Run()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
uc.Email = strings.TrimSpace(res)
|
uc.Email = strings.TrimSpace(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,24 +176,24 @@ func usersAddAction(ctx *cli.Context) error {
|
|||||||
|
|
||||||
func usersDeleteAction(ctx *cli.Context) error {
|
func usersDeleteAction(ctx *cli.Context) error {
|
||||||
return callWithDependencies(ctx, func(conf *config.Config) 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")
|
return errors.New("please provide a username")
|
||||||
}
|
}
|
||||||
|
|
||||||
actionPrompt := promptui.Prompt{
|
actionPrompt := promptui.Prompt{
|
||||||
Label: fmt.Sprintf("Delete %s?", clean.Log(userName)),
|
Label: fmt.Sprintf("Delete %s?", clean.Log(login)),
|
||||||
IsConfirm: true,
|
IsConfirm: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := actionPrompt.Run(); err == nil {
|
if _, err := actionPrompt.Run(); err == nil {
|
||||||
if m := entity.FindUserByName(userName); m == nil {
|
if m := entity.FindUserByLogin(login); m == nil {
|
||||||
return errors.New("user not found")
|
return errors.New("login name not found")
|
||||||
} else if err := m.Delete(); err != nil {
|
} else if err := m.Delete(); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
log.Infof("%s deleted", clean.Log(userName))
|
log.Infof("%s deleted", clean.LogQuote(login))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Infof("keeping user")
|
log.Infof("keeping user")
|
||||||
@@ -205,52 +205,54 @@ func usersDeleteAction(ctx *cli.Context) error {
|
|||||||
|
|
||||||
func usersListAction(ctx *cli.Context) error {
|
func usersListAction(ctx *cli.Context) error {
|
||||||
return callWithDependencies(ctx, func(conf *config.Config) error {
|
return callWithDependencies(ctx, func(conf *config.Config) error {
|
||||||
|
cols := []string{"UID", "Role", "Username", "Email", "Display Name"}
|
||||||
|
|
||||||
users := query.RegisteredUsers()
|
users := query.RegisteredUsers()
|
||||||
|
rows := make([][]string, len(users))
|
||||||
|
|
||||||
log.Infof("found %s", english.Plural(len(users), "user", "users"))
|
log.Infof("found %s", english.Plural(len(users), "user", "users"))
|
||||||
|
|
||||||
fmt.Printf("%-4s %-16s %-16s %-16s\n", "ID", "LOGIN", "NAME", "EMAIL")
|
for i, user := range users {
|
||||||
|
rows[i] = []string{user.UserUID, user.AclRole().String(), user.UserName(), user.UserEmail(), user.RealName()}
|
||||||
for _, user := range users {
|
|
||||||
fmt.Printf("%-4d %-16s %-16s %-16s", user.ID, user.Username(), user.FullName, user.PrimaryEmail)
|
|
||||||
fmt.Printf("\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
result, err := report.Render(rows, cols, report.CliFormat(ctx))
|
||||||
|
|
||||||
|
fmt.Println(result)
|
||||||
|
|
||||||
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func usersUpdateAction(ctx *cli.Context) error {
|
func usersUpdateAction(ctx *cli.Context) error {
|
||||||
return callWithDependencies(ctx, func(conf *config.Config) error {
|
return callWithDependencies(ctx, func(conf *config.Config) error {
|
||||||
username := ctx.Args().First()
|
login := ctx.Args().First()
|
||||||
if username == "" {
|
|
||||||
return errors.New("pass username as argument")
|
if login == "" {
|
||||||
|
return errors.New("please provide the username as argument")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := entity.FindUserByName(username)
|
u := entity.FindUserByLogin(login)
|
||||||
if u == nil {
|
if u == nil {
|
||||||
return errors.New("user not found")
|
return errors.New("username not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
uc := form.UserCreate{
|
uc := form.UserCreate{
|
||||||
FullName: strings.TrimSpace(ctx.String("fullname")),
|
Username: strings.TrimSpace(ctx.String("username")),
|
||||||
Email: strings.TrimSpace(ctx.String("email")),
|
Email: strings.TrimSpace(ctx.String("email")),
|
||||||
Password: strings.TrimSpace(ctx.String("password")),
|
Password: strings.TrimSpace(ctx.String("password")),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ctx.IsSet("email") && len(uc.Email) > 0 {
|
||||||
|
u.Email = uc.Email
|
||||||
|
}
|
||||||
|
|
||||||
if ctx.IsSet("password") {
|
if ctx.IsSet("password") {
|
||||||
err := u.SetPassword(uc.Password)
|
err := u.SetPassword(uc.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("password successfully changed: %s\n", clean.Log(u.Username()))
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := u.Validate(); err != nil {
|
if err := u.Validate(); err != nil {
|
||||||
@@ -261,7 +263,7 @@ func usersUpdateAction(ctx *cli.Context) error {
|
|||||||
return err
|
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
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -112,10 +112,12 @@ func NewConfig(ctx *cli.Context) *Config {
|
|||||||
|
|
||||||
// Initialize package extensions.
|
// Initialize package extensions.
|
||||||
for _, ext := range Extensions() {
|
for _, ext := range Extensions() {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
if err := ext.init(c); err != nil {
|
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 {
|
} 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 (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
"github.com/photoprism/photoprism/pkg/rnd"
|
"github.com/photoprism/photoprism/pkg/rnd"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
@@ -20,9 +21,20 @@ func isBcrypt(s string) bool {
|
|||||||
return b
|
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.
|
// AdminPassword returns the initial admin password.
|
||||||
func (c *Config) AdminPassword() string {
|
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.
|
// 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.
|
// StaticUri is the relative path for serving static content.
|
||||||
const StaticUri = "/static"
|
const StaticUri = "/static"
|
||||||
|
|
||||||
|
// CustomStaticUri is the relative path for serving custom static content.
|
||||||
|
const CustomStaticUri = "/c/static"
|
||||||
|
|
||||||
// MsgSponsor and MsgSignUp provide sponsorship info messages;
|
// MsgSponsor and MsgSignUp provide sponsorship info messages;
|
||||||
// SignUpURL a signup link.
|
// SignUpURL a signup link.
|
||||||
const MsgSponsor = "PhotoPrism® needs your support!"
|
const MsgSponsor = "PhotoPrism® needs your support!"
|
||||||
|
|||||||
@@ -108,8 +108,10 @@ func (c *Config) WallpaperUri() string {
|
|||||||
// Valid URI? Local file?
|
// Valid URI? Local file?
|
||||||
if p := clean.Path(c.options.WallpaperUri); p == "" {
|
if p := clean.Path(c.options.WallpaperUri); p == "" {
|
||||||
c.options.WallpaperUri = ""
|
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)
|
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 {
|
} else {
|
||||||
c.options.WallpaperUri = ""
|
c.options.WallpaperUri = ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||||
@@ -270,7 +272,12 @@ func (c *Config) MigrateDb(runFailed bool, ids []string) {
|
|||||||
entity.SetDbProvider(c)
|
entity.SetDbProvider(c)
|
||||||
entity.InitDb(true, runFailed, ids)
|
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()
|
go entity.SaveErrorMessages()
|
||||||
}
|
}
|
||||||
@@ -281,7 +288,11 @@ func (c *Config) InitTestDb() {
|
|||||||
entity.SetDbProvider(c)
|
entity.SetDbProvider(c)
|
||||||
entity.ResetTestFixtures()
|
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()
|
go entity.SaveErrorMessages()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -424,6 +424,35 @@ func (c *Config) CustomAssetsPath() string {
|
|||||||
return ""
|
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.
|
// LocalesPath returns the translation locales path.
|
||||||
func (c *Config) LocalesPath() string {
|
func (c *Config) LocalesPath() string {
|
||||||
return filepath.Join(c.AssetsPath(), "locales")
|
return filepath.Join(c.AssetsPath(), "locales")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||||||
rows = [][]string{
|
rows = [][]string{
|
||||||
// Authentication.
|
// Authentication.
|
||||||
{"auth-mode", fmt.Sprintf("%s", c.AuthMode())},
|
{"auth-mode", fmt.Sprintf("%s", c.AuthMode())},
|
||||||
|
{"admin-user", c.AdminUser()},
|
||||||
{"admin-password", strings.Repeat("*", utf8.RuneCountInString(c.AdminPassword()))},
|
{"admin-password", strings.Repeat("*", utf8.RuneCountInString(c.AdminPassword()))},
|
||||||
{"public", fmt.Sprintf("%t", c.Public())},
|
{"public", fmt.Sprintf("%t", c.Public())},
|
||||||
|
|
||||||
@@ -167,8 +168,12 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||||||
{"log-filename", c.LogFilename()},
|
{"log-filename", c.LogFilename()},
|
||||||
}
|
}
|
||||||
|
|
||||||
if p := c.CustomAssetsPath(); p != "" {
|
if v := c.CustomAssetsPath(); v != "" {
|
||||||
rows = append(rows, []string{"custom-assets-path", p})
|
rows = append(rows, []string{"custom-assets-path", v})
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := c.CustomStaticUri(); v != "" {
|
||||||
|
rows = append(rows, []string{"custom-static-uri", v})
|
||||||
}
|
}
|
||||||
|
|
||||||
return rows, cols
|
return rows, cols
|
||||||
|
|||||||
@@ -211,6 +211,15 @@ func TestConfig_DetectNSFW(t *testing.T) {
|
|||||||
assert.Equal(t, true, result)
|
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) {
|
func TestConfig_AdminPassword(t *testing.T) {
|
||||||
c := NewConfig(CliTestContext())
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type Options struct {
|
|||||||
PartnerID string `yaml:"-" json:"-" flag:"partner-id"`
|
PartnerID string `yaml:"-" json:"-" flag:"partner-id"`
|
||||||
AuthMode string `yaml:"AuthMode" json:"-" flag:"auth-mode"`
|
AuthMode string `yaml:"AuthMode" json:"-" flag:"auth-mode"`
|
||||||
Public bool `yaml:"Public" json:"-" flag:"public"`
|
Public bool `yaml:"Public" json:"-" flag:"public"`
|
||||||
|
AdminUser string `yaml:"AdminUser" json:"-" flag:"admin-user"`
|
||||||
AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"`
|
AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"`
|
||||||
LogLevel string `yaml:"LogLevel" json:"-" flag:"log-level"`
|
LogLevel string `yaml:"LogLevel" json:"-" flag:"log-level"`
|
||||||
Prod bool `yaml:"Prod" json:"Prod" flag:"prod"`
|
Prod bool `yaml:"Prod" json:"Prod" flag:"prod"`
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ var Flags = CliFlags{
|
|||||||
Value: "password",
|
Value: "password",
|
||||||
EnvVar: "PHOTOPRISM_AUTH_MODE",
|
EnvVar: "PHOTOPRISM_AUTH_MODE",
|
||||||
}},
|
}},
|
||||||
|
CliFlag{
|
||||||
|
Flag: cli.StringFlag{
|
||||||
|
Name: "admin-user, login",
|
||||||
|
Usage: "admin login `USERNAME`",
|
||||||
|
EnvVar: "PHOTOPRISM_ADMIN_USER",
|
||||||
|
Value: "admin",
|
||||||
|
}},
|
||||||
CliFlag{
|
CliFlag{
|
||||||
Flag: cli.StringFlag{
|
Flag: cli.StringFlag{
|
||||||
Name: "admin-password, pw",
|
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) {
|
func TestUserMap_Get(t *testing.T) {
|
||||||
t.Run("get existing user", func(t *testing.T) {
|
t.Run("get existing user", func(t *testing.T) {
|
||||||
r := UserFixtures.Get("alice")
|
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)
|
assert.IsType(t, User{}, r)
|
||||||
})
|
})
|
||||||
t.Run("get not existing user", func(t *testing.T) {
|
t.Run("get not existing user", func(t *testing.T) {
|
||||||
r := UserFixtures.Get("monstera")
|
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)
|
assert.IsType(t, User{}, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -24,12 +24,12 @@ func TestUserMap_Get(t *testing.T) {
|
|||||||
func TestUserMap_Pointer(t *testing.T) {
|
func TestUserMap_Pointer(t *testing.T) {
|
||||||
t.Run("get existing user", func(t *testing.T) {
|
t.Run("get existing user", func(t *testing.T) {
|
||||||
r := UserFixtures.Pointer("alice")
|
r := UserFixtures.Pointer("alice")
|
||||||
assert.Equal(t, "alice", r.UserName)
|
assert.Equal(t, "alice", r.Username)
|
||||||
assert.IsType(t, &User{}, r)
|
assert.IsType(t, &User{}, r)
|
||||||
})
|
})
|
||||||
t.Run("get not existing user", func(t *testing.T) {
|
t.Run("get not existing user", func(t *testing.T) {
|
||||||
r := UserFixtures.Pointer("monstera")
|
r := UserFixtures.Pointer("monstera")
|
||||||
assert.Equal(t, "", r.UserName)
|
assert.Equal(t, "", r.Username)
|
||||||
assert.IsType(t, &User{}, r)
|
assert.IsType(t, &User{}, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
func TestFindUserByName(t *testing.T) {
|
func TestFindUserByName(t *testing.T) {
|
||||||
t.Run("admin", func(t *testing.T) {
|
t.Run("admin", func(t *testing.T) {
|
||||||
m := FindUserByName("admin")
|
m := FindUserByLogin("admin")
|
||||||
|
|
||||||
if m == nil {
|
if m == nil {
|
||||||
t.Fatal("result should not be nil")
|
t.Fatal("result should not be nil")
|
||||||
@@ -19,21 +19,25 @@ func TestFindUserByName(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, 1, m.ID)
|
assert.Equal(t, 1, m.ID)
|
||||||
assert.NotEmpty(t, m.UserUID)
|
assert.NotEmpty(t, m.UserUID)
|
||||||
assert.Equal(t, "admin", m.UserName)
|
assert.Equal(t, "admin", m.Username)
|
||||||
assert.Equal(t, "admin", m.Username())
|
assert.Equal(t, "admin", m.UserName())
|
||||||
m.UserName = "Admin "
|
m.Username = "Admin "
|
||||||
assert.Equal(t, "admin", m.Username())
|
assert.Equal(t, "admin", m.UserName())
|
||||||
assert.Equal(t, "Admin ", m.UserName)
|
assert.Equal(t, "Admin ", m.Username)
|
||||||
assert.Equal(t, "Admin", m.FullName)
|
assert.Equal(t, "Admin", m.DisplayName)
|
||||||
assert.True(t, m.RoleAdmin)
|
assert.Equal(t, acl.RoleAdmin, m.AclRole())
|
||||||
assert.False(t, m.RoleGuest)
|
assert.False(t, m.IsEditor())
|
||||||
assert.False(t, m.UserDisabled)
|
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.CreatedAt)
|
||||||
assert.NotEmpty(t, m.UpdatedAt)
|
assert.NotEmpty(t, m.UpdatedAt)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("alice", func(t *testing.T) {
|
t.Run("alice", func(t *testing.T) {
|
||||||
m := FindUserByName("alice")
|
m := FindUserByLogin("alice")
|
||||||
|
|
||||||
if m == nil {
|
if m == nil {
|
||||||
t.Fatal("result should not be 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, 5, m.ID)
|
||||||
assert.Equal(t, "uqxetse3cy5eo9z2", m.UserUID)
|
assert.Equal(t, "uqxetse3cy5eo9z2", m.UserUID)
|
||||||
assert.Equal(t, "alice", m.UserName)
|
assert.Equal(t, "alice", m.Username)
|
||||||
assert.Equal(t, "Alice", m.FullName)
|
assert.Equal(t, "Alice", m.DisplayName)
|
||||||
assert.Equal(t, "alice@example.com", m.PrimaryEmail)
|
assert.Equal(t, "alice@example.com", m.Email)
|
||||||
assert.True(t, m.RoleAdmin)
|
assert.True(t, m.SuperAdmin)
|
||||||
assert.False(t, m.RoleGuest)
|
assert.Equal(t, acl.RoleAdmin, m.AclRole())
|
||||||
assert.False(t, m.UserDisabled)
|
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.CreatedAt)
|
||||||
assert.NotEmpty(t, m.UpdatedAt)
|
assert.NotEmpty(t, m.UpdatedAt)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("bob", func(t *testing.T) {
|
t.Run("bob", func(t *testing.T) {
|
||||||
m := FindUserByName("bob")
|
m := FindUserByLogin("bob")
|
||||||
|
|
||||||
if m == nil {
|
if m == nil {
|
||||||
t.Fatal("result should not be 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, 7, m.ID)
|
||||||
assert.Equal(t, "uqxc08w3d0ej2283", m.UserUID)
|
assert.Equal(t, "uqxc08w3d0ej2283", m.UserUID)
|
||||||
assert.Equal(t, "bob", m.UserName)
|
assert.Equal(t, "bob", m.Username)
|
||||||
assert.Equal(t, "Bob", m.FullName)
|
assert.Equal(t, "Bob", m.DisplayName)
|
||||||
assert.Equal(t, "bob@example.com", m.PrimaryEmail)
|
assert.Equal(t, "bob@example.com", m.Email)
|
||||||
assert.False(t, m.RoleAdmin)
|
assert.False(t, m.SuperAdmin)
|
||||||
assert.False(t, m.RoleGuest)
|
assert.True(t, m.IsEditor())
|
||||||
assert.False(t, m.UserDisabled)
|
assert.False(t, m.IsViewer())
|
||||||
|
assert.False(t, m.IsGuest())
|
||||||
|
assert.True(t, m.CanLogin)
|
||||||
assert.NotEmpty(t, m.CreatedAt)
|
assert.NotEmpty(t, m.CreatedAt)
|
||||||
assert.NotEmpty(t, m.UpdatedAt)
|
assert.NotEmpty(t, m.UpdatedAt)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unknown", func(t *testing.T) {
|
t.Run("unknown", func(t *testing.T) {
|
||||||
m := FindUserByName("")
|
m := FindUserByLogin("")
|
||||||
|
|
||||||
if m != nil {
|
if m != nil {
|
||||||
t.Fatal("result should be nil")
|
t.Fatal("result should be nil")
|
||||||
@@ -79,7 +89,7 @@ func TestFindUserByName(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("not found", func(t *testing.T) {
|
t.Run("not found", func(t *testing.T) {
|
||||||
m := FindUserByName("xxx")
|
m := FindUserByLogin("xxx")
|
||||||
|
|
||||||
if m != nil {
|
if m != nil {
|
||||||
t.Fatal("result should be 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) {
|
func TestUser_InvalidPassword(t *testing.T) {
|
||||||
t.Run("admin", func(t *testing.T) {
|
t.Run("admin", func(t *testing.T) {
|
||||||
m := FindUserByName("admin")
|
m := FindUserByLogin("admin")
|
||||||
|
|
||||||
if m == nil {
|
if m == nil {
|
||||||
t.Fatal("result should not be nil")
|
t.Fatal("result should not be nil")
|
||||||
@@ -98,7 +154,7 @@ func TestUser_InvalidPassword(t *testing.T) {
|
|||||||
assert.False(t, m.InvalidPassword("photoprism"))
|
assert.False(t, m.InvalidPassword("photoprism"))
|
||||||
})
|
})
|
||||||
t.Run("admin invalid password", func(t *testing.T) {
|
t.Run("admin invalid password", func(t *testing.T) {
|
||||||
m := FindUserByName("admin")
|
m := FindUserByLogin("admin")
|
||||||
|
|
||||||
if m == nil {
|
if m == nil {
|
||||||
t.Fatal("result should not be 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"))
|
assert.True(t, m.InvalidPassword("wrong-password"))
|
||||||
})
|
})
|
||||||
t.Run("no password existing", func(t *testing.T) {
|
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()
|
err := p.Save()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -116,11 +172,11 @@ func TestUser_InvalidPassword(t *testing.T) {
|
|||||||
|
|
||||||
})
|
})
|
||||||
t.Run("not registered", func(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"))
|
assert.True(t, p.InvalidPassword("abcdef"))
|
||||||
})
|
})
|
||||||
t.Run("password empty", func(t *testing.T) {
|
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(""))
|
assert.True(t, p.InvalidPassword(""))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -176,8 +232,8 @@ func TestFindUserByUID(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, -2, m.ID)
|
assert.Equal(t, -2, m.ID)
|
||||||
assert.NotEmpty(t, m.UserUID)
|
assert.NotEmpty(t, m.UserUID)
|
||||||
assert.Equal(t, "", m.UserName)
|
assert.Equal(t, "", m.Username)
|
||||||
assert.Equal(t, "Guest", m.FullName)
|
assert.Equal(t, "Guest", m.DisplayName)
|
||||||
assert.NotEmpty(t, m.CreatedAt)
|
assert.NotEmpty(t, m.CreatedAt)
|
||||||
assert.NotEmpty(t, m.UpdatedAt)
|
assert.NotEmpty(t, m.UpdatedAt)
|
||||||
})
|
})
|
||||||
@@ -207,12 +263,15 @@ func TestFindUserByUID(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, 5, m.ID)
|
assert.Equal(t, 5, m.ID)
|
||||||
assert.Equal(t, "uqxetse3cy5eo9z2", m.UserUID)
|
assert.Equal(t, "uqxetse3cy5eo9z2", m.UserUID)
|
||||||
assert.Equal(t, "alice", m.Username())
|
assert.Equal(t, "alice", m.UserName())
|
||||||
assert.Equal(t, "Alice", m.FullName)
|
assert.Equal(t, "Alice", m.DisplayName)
|
||||||
assert.Equal(t, "alice@example.com", m.PrimaryEmail)
|
assert.Equal(t, "alice@example.com", m.Email)
|
||||||
assert.True(t, m.RoleAdmin)
|
assert.True(t, m.SuperAdmin)
|
||||||
assert.False(t, m.RoleGuest)
|
assert.True(t, m.IsAdmin())
|
||||||
assert.False(t, m.UserDisabled)
|
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.CreatedAt)
|
||||||
assert.NotEmpty(t, m.UpdatedAt)
|
assert.NotEmpty(t, m.UpdatedAt)
|
||||||
})
|
})
|
||||||
@@ -226,12 +285,15 @@ func TestFindUserByUID(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, 7, m.ID)
|
assert.Equal(t, 7, m.ID)
|
||||||
assert.Equal(t, "uqxc08w3d0ej2283", m.UserUID)
|
assert.Equal(t, "uqxc08w3d0ej2283", m.UserUID)
|
||||||
assert.Equal(t, "bob", m.UserName)
|
assert.Equal(t, "bob", m.Username)
|
||||||
assert.Equal(t, "Bob", m.FullName)
|
assert.Equal(t, "Bob", m.DisplayName)
|
||||||
assert.Equal(t, "bob@example.com", m.PrimaryEmail)
|
assert.Equal(t, "bob@example.com", m.Email)
|
||||||
assert.False(t, m.RoleAdmin)
|
assert.False(t, m.SuperAdmin)
|
||||||
assert.False(t, m.RoleGuest)
|
assert.False(t, m.IsAdmin())
|
||||||
assert.False(t, m.UserDisabled)
|
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.CreatedAt)
|
||||||
assert.NotEmpty(t, m.UpdatedAt)
|
assert.NotEmpty(t, m.UpdatedAt)
|
||||||
})
|
})
|
||||||
@@ -244,10 +306,12 @@ func TestFindUserByUID(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, 8, m.ID)
|
assert.Equal(t, 8, m.ID)
|
||||||
assert.Equal(t, "uqxqg7i1kperxvu7", m.UserUID)
|
assert.Equal(t, "uqxqg7i1kperxvu7", m.UserUID)
|
||||||
assert.False(t, m.RoleAdmin)
|
assert.False(t, m.SuperAdmin)
|
||||||
assert.False(t, m.RoleGuest)
|
assert.False(t, m.IsAdmin())
|
||||||
assert.True(t, m.RoleFriend)
|
assert.False(t, m.IsEditor())
|
||||||
assert.True(t, m.UserDisabled)
|
assert.True(t, m.IsViewer())
|
||||||
|
assert.False(t, m.IsGuest())
|
||||||
|
assert.True(t, m.CanLogin)
|
||||||
assert.NotEmpty(t, m.CreatedAt)
|
assert.NotEmpty(t, m.CreatedAt)
|
||||||
assert.NotEmpty(t, m.UpdatedAt)
|
assert.NotEmpty(t, m.UpdatedAt)
|
||||||
})
|
})
|
||||||
@@ -256,75 +320,79 @@ func TestFindUserByUID(t *testing.T) {
|
|||||||
|
|
||||||
func TestUser_String(t *testing.T) {
|
func TestUser_String(t *testing.T) {
|
||||||
t.Run("UID", func(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())
|
assert.Equal(t, "abc123", p.String())
|
||||||
})
|
})
|
||||||
t.Run("DisplayName", func(t *testing.T) {
|
t.Run("FullName", func(t *testing.T) {
|
||||||
p := User{UserUID: "abc123", UserName: "", FullName: "Test"}
|
p := User{UserUID: "abc123", Username: "", DisplayName: "Test"}
|
||||||
assert.Equal(t, "Test", p.String())
|
assert.Equal(t, "Test", p.String())
|
||||||
})
|
})
|
||||||
t.Run("UserName", func(t *testing.T) {
|
t.Run("Username", func(t *testing.T) {
|
||||||
p := User{UserUID: "abc123", UserName: "Super-User ", FullName: "Test"}
|
p := User{UserUID: "abc123", Username: "Super-User ", DisplayName: "Test"}
|
||||||
assert.Equal(t, "super-user", p.String())
|
assert.Equal(t, "super-user", p.String())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUser_Admin(t *testing.T) {
|
func TestUser_Admin(t *testing.T) {
|
||||||
t.Run("true", func(t *testing.T) {
|
t.Run("SuperAdmin", func(t *testing.T) {
|
||||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: "", RoleAdmin: true}
|
p := User{UserUID: "u000000000000008", Username: "Hanna", DisplayName: "", SuperAdmin: true}
|
||||||
assert.True(t, p.Admin())
|
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) {
|
t.Run("false", func(t *testing.T) {
|
||||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: "", RoleAdmin: false}
|
p := User{UserUID: "u000000000000008", Username: "Hanna", DisplayName: "", SuperAdmin: false, UserRole: ""}
|
||||||
assert.False(t, p.Admin())
|
assert.False(t, p.IsAdmin())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUser_Anonymous(t *testing.T) {
|
func TestUser_Anonymous(t *testing.T) {
|
||||||
t.Run("true", func(t *testing.T) {
|
t.Run("true", func(t *testing.T) {
|
||||||
p := User{UserUID: "", UserName: "Hanna", FullName: ""}
|
p := User{UserUID: "", Username: "Hanna", DisplayName: ""}
|
||||||
assert.True(t, p.Anonymous())
|
assert.True(t, p.IsAnonymous())
|
||||||
})
|
})
|
||||||
t.Run("false", func(t *testing.T) {
|
t.Run("false", func(t *testing.T) {
|
||||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: "", RoleAdmin: true}
|
p := User{UserUID: "u000000000000008", Username: "Hanna", DisplayName: "", SuperAdmin: true}
|
||||||
assert.False(t, p.Anonymous())
|
assert.False(t, p.IsAnonymous())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUser_Guest(t *testing.T) {
|
func TestUser_Guest(t *testing.T) {
|
||||||
t.Run("true", func(t *testing.T) {
|
t.Run("true", func(t *testing.T) {
|
||||||
p := User{UserUID: "", UserName: "Hanna", FullName: "", RoleGuest: true}
|
p := User{UserUID: "", Username: "Hanna", DisplayName: "", UserRole: "guest"}
|
||||||
assert.True(t, p.Guest())
|
assert.True(t, p.IsGuest())
|
||||||
})
|
})
|
||||||
t.Run("false", func(t *testing.T) {
|
t.Run("false", func(t *testing.T) {
|
||||||
p := User{UserUID: "", UserName: "Hanna", FullName: ""}
|
p := User{UserUID: "", Username: "Hanna", DisplayName: ""}
|
||||||
assert.False(t, p.Guest())
|
assert.False(t, p.IsGuest())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUser_SetPassword(t *testing.T) {
|
func TestUser_SetPassword(t *testing.T) {
|
||||||
t.Run("success", func(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 {
|
if err := p.SetPassword("insecure"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("not registered", func(t *testing.T) {
|
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"))
|
assert.Error(t, p.SetPassword("insecure"))
|
||||||
|
|
||||||
})
|
})
|
||||||
t.Run("password too short", func(t *testing.T) {
|
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"))
|
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) {
|
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"))
|
assert.Nil(t, FindPassword("u000000000000009"))
|
||||||
p.InitPassword("insecure")
|
p.InitAccount("admin", "insecure")
|
||||||
m := FindPassword("u000000000000009")
|
m := FindPassword("u000000000000009")
|
||||||
|
|
||||||
if m == nil {
|
if m == nil {
|
||||||
@@ -332,7 +400,7 @@ func TestUser_InitPassword(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("already existing", func(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 {
|
if err := p.Save(); err != nil {
|
||||||
t.Logf("cannot user %s: ", err)
|
t.Logf("cannot user %s: ", err)
|
||||||
@@ -343,7 +411,7 @@ func TestUser_InitPassword(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert.NotNil(t, FindPassword("u000000000000010"))
|
assert.NotNil(t, FindPassword("u000000000000010"))
|
||||||
p.InitPassword("insecure")
|
p.InitAccount("admin", "insecure")
|
||||||
m := FindPassword("u000000000000010")
|
m := FindPassword("u000000000000010")
|
||||||
|
|
||||||
if m == nil {
|
if m == nil {
|
||||||
@@ -351,129 +419,112 @@ func TestUser_InitPassword(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("not registered", func(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"))
|
assert.Nil(t, FindPassword("u12"))
|
||||||
p.InitPassword("insecure")
|
p.InitAccount("admin", "insecure")
|
||||||
assert.Nil(t, FindPassword("u12"))
|
assert.Nil(t, FindPassword("u12"))
|
||||||
})
|
})
|
||||||
t.Run("password empty", func(t *testing.T) {
|
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"))
|
assert.Nil(t, FindPassword("u000000000000011"))
|
||||||
p.InitPassword("")
|
p.InitAccount("admin", "")
|
||||||
assert.Nil(t, FindPassword("u000000000000011"))
|
assert.Nil(t, FindPassword("u000000000000011"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUser_Role(t *testing.T) {
|
func TestUser_Role(t *testing.T) {
|
||||||
t.Run("admin", func(t *testing.T) {
|
t.Run("admin", func(t *testing.T) {
|
||||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: "", RoleAdmin: true}
|
p := User{UserUID: "u000000000000008", Username: "Hanna", DisplayName: "", SuperAdmin: true, UserRole: acl.RoleAdmin.String()}
|
||||||
assert.Equal(t, acl.Role("admin"), p.Role())
|
assert.Equal(t, acl.Role("admin"), p.AclRole())
|
||||||
})
|
assert.True(t, p.IsAdmin())
|
||||||
t.Run("child", func(t *testing.T) {
|
assert.False(t, p.IsGuest())
|
||||||
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())
|
|
||||||
})
|
})
|
||||||
t.Run("guest", func(t *testing.T) {
|
t.Run("guest", func(t *testing.T) {
|
||||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: "", RoleGuest: true}
|
p := User{UserUID: "u000000000000008", Username: "Hanna", DisplayName: "", UserRole: acl.RoleGuest.String()}
|
||||||
assert.Equal(t, acl.Role("guest"), p.Role())
|
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) {
|
t.Run("default", func(t *testing.T) {
|
||||||
p := User{UserUID: "u000000000000008", UserName: "Hanna", FullName: ""}
|
p := User{UserUID: "u000000000000008", Username: "Hanna", DisplayName: ""}
|
||||||
assert.Equal(t, acl.Role("*"), p.Role())
|
assert.Equal(t, acl.Role("*"), p.AclRole())
|
||||||
|
assert.False(t, p.IsAdmin())
|
||||||
|
assert.False(t, p.IsGuest())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUser_Validate(t *testing.T) {
|
func TestUser_Validate(t *testing.T) {
|
||||||
t.Run("valid", func(t *testing.T) {
|
t.Run("valid", func(t *testing.T) {
|
||||||
u := &User{
|
u := &User{
|
||||||
AddressID: 1,
|
Username: "validate",
|
||||||
UserName: "validate",
|
DisplayName: "Validate",
|
||||||
FullName: "Validate",
|
Email: "validate@example.com",
|
||||||
PrimaryEmail: "validate@example.com",
|
|
||||||
}
|
}
|
||||||
err := u.Validate()
|
err := u.Validate()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
})
|
})
|
||||||
t.Run("username empty", func(t *testing.T) {
|
t.Run("username empty", func(t *testing.T) {
|
||||||
u := &User{
|
u := &User{
|
||||||
AddressID: 1,
|
Username: "",
|
||||||
UserName: "",
|
DisplayName: "Validate",
|
||||||
FullName: "Validate",
|
Email: "validate@example.com",
|
||||||
PrimaryEmail: "validate@example.com",
|
|
||||||
}
|
}
|
||||||
err := u.Validate()
|
err := u.Validate()
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
t.Run("username too short", func(t *testing.T) {
|
t.Run("username too short", func(t *testing.T) {
|
||||||
u := &User{
|
u := &User{
|
||||||
AddressID: 1,
|
Username: "va",
|
||||||
UserName: "va",
|
DisplayName: "Validate",
|
||||||
FullName: "Validate",
|
Email: "validate@example.com",
|
||||||
PrimaryEmail: "validate@example.com",
|
|
||||||
}
|
}
|
||||||
err := u.Validate()
|
err := u.Validate()
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
t.Run("username not unique", func(t *testing.T) {
|
t.Run("username not unique", func(t *testing.T) {
|
||||||
FirstOrCreateUser(&User{
|
FirstOrCreateUser(&User{
|
||||||
AddressID: 1,
|
Username: "notunique1",
|
||||||
UserName: "notunique1",
|
|
||||||
})
|
})
|
||||||
u := &User{
|
u := &User{
|
||||||
AddressID: 1,
|
Username: "notunique1",
|
||||||
UserName: "notunique1",
|
DisplayName: "Not Unique",
|
||||||
FullName: "Not Unique",
|
Email: "notunique1@example.com",
|
||||||
PrimaryEmail: "notunique1@example.com",
|
|
||||||
}
|
}
|
||||||
assert.Error(t, u.Validate())
|
assert.Error(t, u.Validate())
|
||||||
})
|
})
|
||||||
t.Run("email not unique", func(t *testing.T) {
|
t.Run("email not unique", func(t *testing.T) {
|
||||||
FirstOrCreateUser(&User{
|
FirstOrCreateUser(&User{
|
||||||
AddressID: 1,
|
Email: "notunique2@example.com",
|
||||||
PrimaryEmail: "notunique2@example.com",
|
|
||||||
})
|
})
|
||||||
u := &User{
|
u := &User{
|
||||||
AddressID: 1,
|
Username: "notunique2",
|
||||||
UserName: "notunique2",
|
Email: "notunique2@example.com",
|
||||||
FullName: "Not Unique",
|
DisplayName: "Not Unique",
|
||||||
PrimaryEmail: "notunique2@example.com",
|
|
||||||
}
|
}
|
||||||
assert.Error(t, u.Validate())
|
assert.Error(t, u.Validate())
|
||||||
})
|
})
|
||||||
t.Run("update user - email not unique", func(t *testing.T) {
|
t.Run("update user - email not unique", func(t *testing.T) {
|
||||||
FirstOrCreateUser(&User{
|
FirstOrCreateUser(&User{
|
||||||
AddressID: 1,
|
Username: "notunique3",
|
||||||
UserName: "notunique3",
|
Email: "notunique3@example.com",
|
||||||
FullName: "Not Unique",
|
DisplayName: "Not Unique",
|
||||||
PrimaryEmail: "notunique3@example.com",
|
|
||||||
})
|
})
|
||||||
u := FirstOrCreateUser(&User{
|
u := FirstOrCreateUser(&User{
|
||||||
AddressID: 1,
|
Username: "notunique30",
|
||||||
UserName: "notunique30",
|
Email: "notunique3@example.com",
|
||||||
FullName: "Not Unique",
|
DisplayName: "Not Unique",
|
||||||
PrimaryEmail: "notunique3@example.com",
|
|
||||||
})
|
})
|
||||||
u.UserName = "notunique3"
|
u.Username = "notunique3"
|
||||||
assert.Error(t, u.Validate())
|
assert.Error(t, u.Validate())
|
||||||
})
|
})
|
||||||
t.Run("primary email empty", func(t *testing.T) {
|
t.Run("primary email empty", func(t *testing.T) {
|
||||||
FirstOrCreateUser(&User{
|
FirstOrCreateUser(&User{
|
||||||
AddressID: 1,
|
Username: "nnomail",
|
||||||
UserName: "nnomail",
|
|
||||||
})
|
})
|
||||||
u := &User{
|
u := &User{
|
||||||
AddressID: 1,
|
Username: "nomail",
|
||||||
UserName: "nomail",
|
Email: "",
|
||||||
FullName: "No Mail",
|
DisplayName: "No Mail",
|
||||||
PrimaryEmail: "",
|
|
||||||
}
|
}
|
||||||
assert.Nil(t, u.Validate())
|
assert.Nil(t, u.Validate())
|
||||||
})
|
})
|
||||||
@@ -482,21 +533,21 @@ func TestUser_Validate(t *testing.T) {
|
|||||||
func TestCreateWithPassword(t *testing.T) {
|
func TestCreateWithPassword(t *testing.T) {
|
||||||
t.Run("password too short", func(t *testing.T) {
|
t.Run("password too short", func(t *testing.T) {
|
||||||
u := form.UserCreate{
|
u := form.UserCreate{
|
||||||
UserName: "thomas1",
|
Username: "thomas1",
|
||||||
FullName: "Thomas One",
|
|
||||||
Email: "thomas1@example.com",
|
Email: "thomas1@example.com",
|
||||||
Password: "hel",
|
Password: "hel",
|
||||||
}
|
}
|
||||||
|
|
||||||
err := CreateWithPassword(u)
|
err := CreateWithPassword(u)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
t.Run("valid", func(t *testing.T) {
|
t.Run("valid", func(t *testing.T) {
|
||||||
u := form.UserCreate{
|
u := form.UserCreate{
|
||||||
UserName: "thomas2",
|
Username: "thomas2",
|
||||||
FullName: "Thomas Two",
|
|
||||||
Email: "thomas2@example.com",
|
Email: "thomas2@example.com",
|
||||||
Password: "helloworld",
|
Password: "helloworld",
|
||||||
}
|
}
|
||||||
|
|
||||||
err := CreateWithPassword(u)
|
err := CreateWithPassword(u)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
})
|
})
|
||||||
@@ -505,22 +556,22 @@ func TestCreateWithPassword(t *testing.T) {
|
|||||||
func TestDeleteUser(t *testing.T) {
|
func TestDeleteUser(t *testing.T) {
|
||||||
t.Run("delete user - success", func(t *testing.T) {
|
t.Run("delete user - success", func(t *testing.T) {
|
||||||
u := &User{
|
u := &User{
|
||||||
AddressID: 1,
|
Username: "thomasdel",
|
||||||
UserName: "thomasdel",
|
Email: "thomasdel@example.com",
|
||||||
FullName: "Thomas Delete",
|
DisplayName: "Thomas Delete",
|
||||||
PrimaryEmail: "thomasdel@example.com",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u = FirstOrCreateUser(u)
|
u = FirstOrCreateUser(u)
|
||||||
err := u.Delete()
|
err := u.Delete()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
t.Run("delete user - not in db", func(t *testing.T) {
|
t.Run("delete user - not in db", func(t *testing.T) {
|
||||||
u := &User{
|
u := &User{
|
||||||
AddressID: 1,
|
Username: "thomasdel2",
|
||||||
UserName: "thomasdel2",
|
Email: "thomasdel2@example.com",
|
||||||
FullName: "Thomas Delete 2",
|
DisplayName: "Thomas Delete 2",
|
||||||
PrimaryEmail: "thomasdel2@example.com",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := u.Delete()
|
err := u.Delete()
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
@@ -18,6 +18,11 @@ type Duplicate struct {
|
|||||||
ModTime int64 `json:"ModTime" yaml:"-"`
|
ModTime int64 `json:"ModTime" yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TableName returns the entity database table name.
|
||||||
|
func (Duplicate) TableName() string {
|
||||||
|
return "duplicates"
|
||||||
|
}
|
||||||
|
|
||||||
// AddDuplicate adds a duplicate.
|
// AddDuplicate adds a duplicate.
|
||||||
func AddDuplicate(fileName, fileRoot, fileHash string, fileSize, modTime int64) error {
|
func AddDuplicate(fileName, fileRoot, fileHash string, fileSize, modTime int64) error {
|
||||||
if fileName == "" {
|
if fileName == "" {
|
||||||
|
|||||||
@@ -69,3 +69,11 @@ const (
|
|||||||
SortOrderCategory = "category"
|
SortOrderCategory = "category"
|
||||||
SortOrderSimilar = "similar"
|
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.
|
// CreateDefaultFixtures inserts default fixtures for test and production.
|
||||||
func CreateDefaultFixtures() {
|
func CreateDefaultFixtures() {
|
||||||
CreateUnknownAddress()
|
|
||||||
CreateDefaultUsers()
|
CreateDefaultUsers()
|
||||||
CreateUnknownPlace()
|
CreateUnknownPlace()
|
||||||
CreateUnknownLocation()
|
CreateUnknownLocation()
|
||||||
|
|||||||
@@ -15,31 +15,31 @@ type Tables map[string]interface{}
|
|||||||
// Entities contains database entities and their table names.
|
// Entities contains database entities and their table names.
|
||||||
var Entities = Tables{
|
var Entities = Tables{
|
||||||
migrate.Migration{}.TableName(): &migrate.Migration{},
|
migrate.Migration{}.TableName(): &migrate.Migration{},
|
||||||
"errors": &Error{},
|
Error{}.TableName(): &Error{},
|
||||||
"addresses": &Address{},
|
Password{}.TableName(): &Password{},
|
||||||
"users": &User{},
|
User{}.TableName(): &User{},
|
||||||
"accounts": &Account{},
|
Token{}.TableName(): &Token{},
|
||||||
"folders": &Folder{},
|
Account{}.TableName(): &Account{},
|
||||||
"duplicates": &Duplicate{},
|
Folder{}.TableName(): &Folder{},
|
||||||
|
Duplicate{}.TableName(): &Duplicate{},
|
||||||
File{}.TableName(): &File{},
|
File{}.TableName(): &File{},
|
||||||
"files_share": &FileShare{},
|
FileShare{}.TableName(): &FileShare{},
|
||||||
"files_sync": &FileSync{},
|
FileSync{}.TableName(): &FileSync{},
|
||||||
Photo{}.TableName(): &Photo{},
|
Photo{}.TableName(): &Photo{},
|
||||||
"details": &Details{},
|
Details{}.TableName(): &Details{},
|
||||||
Place{}.TableName(): &Place{},
|
Place{}.TableName(): &Place{},
|
||||||
Cell{}.TableName(): &Cell{},
|
Cell{}.TableName(): &Cell{},
|
||||||
"cameras": &Camera{},
|
Camera{}.TableName(): &Camera{},
|
||||||
"lenses": &Lens{},
|
Lens{}.TableName(): &Lens{},
|
||||||
"countries": &Country{},
|
Country{}.TableName(): &Country{},
|
||||||
"albums": &Album{},
|
Album{}.TableName(): &Album{},
|
||||||
"photos_albums": &PhotoAlbum{},
|
PhotoAlbum{}.TableName(): &PhotoAlbum{},
|
||||||
"labels": &Label{},
|
Label{}.TableName(): &Label{},
|
||||||
"categories": &Category{},
|
Category{}.TableName(): &Category{},
|
||||||
"photos_labels": &PhotoLabel{},
|
PhotoLabel{}.TableName(): &PhotoLabel{},
|
||||||
"keywords": &Keyword{},
|
Keyword{}.TableName(): &Keyword{},
|
||||||
"photos_keywords": &PhotoKeyword{},
|
PhotoKeyword{}.TableName(): &PhotoKeyword{},
|
||||||
"passwords": &Password{},
|
Link{}.TableName(): &Link{},
|
||||||
"links": &Link{},
|
|
||||||
Subject{}.TableName(): &Subject{},
|
Subject{}.TableName(): &Subject{},
|
||||||
Face{}.TableName(): &Face{},
|
Face{}.TableName(): &Face{},
|
||||||
Marker{}.TableName(): &Marker{},
|
Marker{}.TableName(): &Marker{},
|
||||||
@@ -73,7 +73,15 @@ func (list Tables) WaitForMigration(db *gorm.DB) {
|
|||||||
|
|
||||||
// Truncate removes all data from tables without dropping them.
|
// Truncate removes all data from tables without dropping them.
|
||||||
func (list Tables) Truncate(db *gorm.DB) {
|
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 {
|
if err := db.Exec(fmt.Sprintf("DELETE FROM %s WHERE 1", name)).Error; err == nil {
|
||||||
// log.Debugf("entity: removed all data from %s", name)
|
// log.Debugf("entity: removed all data from %s", name)
|
||||||
break
|
break
|
||||||
@@ -85,14 +93,23 @@ func (list Tables) Truncate(db *gorm.DB) {
|
|||||||
|
|
||||||
// Migrate migrates all database tables of registered entities.
|
// Migrate migrates all database tables of registered entities.
|
||||||
func (list Tables) Migrate(db *gorm.DB, runFailed bool, ids []string) {
|
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 {
|
if len(ids) == 0 {
|
||||||
for name, entity := range list {
|
for name, entity = range list {
|
||||||
if err := db.AutoMigrate(entity).Error; err != nil {
|
if err := db.AutoMigrate(entity).Error; err != nil {
|
||||||
log.Debugf("migrate: %s (waiting 1s)", err.Error())
|
log.Debugf("migrate: %s (waiting 1s)", err.Error())
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
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))
|
log.Errorf("migrate: failed migrating %s", clean.Log(name))
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ type Keyword struct {
|
|||||||
Skip bool
|
Skip bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TableName returns the entity database table name.
|
||||||
|
func (Keyword) TableName() string {
|
||||||
|
return "keywords"
|
||||||
|
}
|
||||||
|
|
||||||
// NewKeyword registers a new keyword in database
|
// NewKeyword registers a new keyword in database
|
||||||
func NewKeyword(keyword string) *Keyword {
|
func NewKeyword(keyword string) *Keyword {
|
||||||
keyword = strings.ToLower(txt.Clip(keyword, txt.ClipKeyword))
|
keyword = strings.ToLower(txt.Clip(keyword, txt.ClipKeyword))
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ type Link struct {
|
|||||||
ModifiedAt time.Time `deepcopier:"skip" json:"ModifiedAt" yaml:"ModifiedAt"`
|
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.
|
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||||
func (m *Link) BeforeCreate(scope *gorm.Scope) error {
|
func (m *Link) BeforeCreate(scope *gorm.Scope) error {
|
||||||
if rnd.ValidID(m.LinkUID, 's') {
|
if rnd.ValidID(m.LinkUID, 's') {
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ type Password struct {
|
|||||||
UpdatedAt time.Time `deepcopier:"skip" json:"UpdatedAt"`
|
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.
|
// NewPassword creates a new password instance.
|
||||||
func NewPassword(uid, password string) Password {
|
func NewPassword(uid, password string) Password {
|
||||||
if uid == "" {
|
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
|
package form
|
||||||
|
|
||||||
type Login struct {
|
type Login struct {
|
||||||
|
Username string `json:"username"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
UserName string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
@@ -11,8 +11,8 @@ func (f Login) HasToken() bool {
|
|||||||
return f.Token != ""
|
return f.Token != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f Login) HasUserName() bool {
|
func (f Login) HasUsername() bool {
|
||||||
return f.UserName != "" && len(f.UserName) <= 255
|
return f.Username != "" && len(f.Username) <= 255
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f Login) HasPassword() bool {
|
func (f Login) HasPassword() bool {
|
||||||
@@ -20,5 +20,5 @@ func (f Login) HasPassword() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f Login) HasCredentials() 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) {
|
func TestLogin_HasToken(t *testing.T) {
|
||||||
t.Run("false", func(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())
|
assert.Equal(t, false, form.HasToken())
|
||||||
})
|
})
|
||||||
t.Run("true", func(t *testing.T) {
|
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())
|
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) {
|
t.Run("false", func(t *testing.T) {
|
||||||
form := &Login{Email: "test@test.com", Password: "passwd", Token: ""}
|
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) {
|
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.HasUserName())
|
assert.Equal(t, true, form.HasUsername())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ func TestLogin_HasPassword(t *testing.T) {
|
|||||||
assert.Equal(t, false, form.HasPassword())
|
assert.Equal(t, false, form.HasPassword())
|
||||||
})
|
})
|
||||||
t.Run("true", func(t *testing.T) {
|
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())
|
assert.Equal(t, true, form.HasPassword())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ func TestLogin_HasCredentials(t *testing.T) {
|
|||||||
assert.Equal(t, false, form.HasCredentials())
|
assert.Equal(t, false, form.HasCredentials())
|
||||||
})
|
})
|
||||||
t.Run("true", func(t *testing.T) {
|
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())
|
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.
|
// UserCreate represents a User with a new password.
|
||||||
type UserCreate struct {
|
type UserCreate struct {
|
||||||
UserName string `json:"username"`
|
Username string `json:"username"`
|
||||||
FullName string `json:"fullname"`
|
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Username returns the normalized username in lowercase and without whitespace padding.
|
// UsernameClean returns the username in lowercase and with whitespace trimmed.
|
||||||
func (f UserCreate) Username() string {
|
func (f UserCreate) UsernameClean() string {
|
||||||
return clean.Username(f.UserName)
|
return clean.Login(f.Username)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ var DialectMySQL = Migrations{
|
|||||||
{
|
{
|
||||||
ID: "20220329-060000",
|
ID: "20220329-060000",
|
||||||
Dialect: "mysql",
|
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",
|
ID: "20220329-061000",
|
||||||
@@ -98,4 +98,9 @@ var DialectMySQL = Migrations{
|
|||||||
Dialect: "mysql",
|
Dialect: "mysql",
|
||||||
Statements: []string{"ALTER TABLE files MODIFY file_chroma SMALLINT DEFAULT -1;"},
|
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",
|
Dialect: "sqlite3",
|
||||||
Statements: []string{"CREATE INDEX IF NOT EXISTS idx_files_missing_root ON files (file_missing, file_root);"},
|
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)
|
log.Tracef("migrate: %s (ignored, probably didn't exist anymore)", err)
|
||||||
} else if strings.HasPrefix(q, "DROP TABLE ") &&
|
} else if strings.HasPrefix(q, "DROP TABLE ") &&
|
||||||
strings.Contains(e, "DROP") {
|
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 {
|
} else {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
ALTER TABLE accounts MODIFY acc_url VARCHAR(255);
|
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_caption VARCHAR(1024);
|
||||||
ALTER TABLE albums MODIFY album_description VARCHAR(2048);
|
ALTER TABLE albums MODIFY album_description VARCHAR(2048);
|
||||||
ALTER TABLE albums MODIFY album_notes VARCHAR(1024);
|
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()
|
users := RegisteredUsers()
|
||||||
|
|
||||||
for _, user := range users {
|
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))
|
t.Logf("user count: %v", len(users))
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func BasicAuth() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user := entity.FindUserByName(username)
|
user := entity.FindUserByLogin(username)
|
||||||
|
|
||||||
if user != nil {
|
if user != nil {
|
||||||
invalid = user.InvalidPassword(password)
|
invalid = user.InvalidPassword(password)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (s Data) Valid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s Data) Guest() bool {
|
func (s Data) Guest() bool {
|
||||||
return s.User.Guest()
|
return s.User.IsGuest()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Data) NoShares() bool {
|
func (s Data) NoShares() bool {
|
||||||
|
|||||||
Reference in New Issue
Block a user