diff --git a/Makefile b/Makefile
index 960be935b..1c88ea6c6 100644
--- a/Makefile
+++ b/Makefile
@@ -507,6 +507,9 @@ fmt-go:
goimports -w pkg internal cmd
tidy:
go mod tidy -go=1.16 && go mod tidy -go=1.17
+users:
+ ./photoprism users add -p photoprism -r admin -s -a test:true -n "Alice Austen" superadmin
+ ./photoprism users ls
# Declare all targets as "PHONY", see https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html.
MAKEFLAGS += --always-make
diff --git a/frontend/src/app/routes.js b/frontend/src/app/routes.js
index f8e72c5d1..4e1102c5f 100644
--- a/frontend/src/app/routes.js
+++ b/frontend/src/app/routes.js
@@ -233,6 +233,12 @@ export default [
component: Places,
meta: { title: $gettext("Places"), auth: true },
},
+ {
+ name: "album_place",
+ path: "/places/:album/:q",
+ component: Places,
+ meta: { title: $gettext("Places"), auth: true },
+ },
{
name: "states",
path: "/states",
diff --git a/frontend/src/common/config.js b/frontend/src/common/config.js
index 8eb9da589..db408d8d6 100644
--- a/frontend/src/common/config.js
+++ b/frontend/src/common/config.js
@@ -355,18 +355,45 @@ export default class Config {
return result;
}
+ // allow checks whether the current user is granted permission for the specified resource.
allow(resource, perm) {
if (this.values["acl"] && this.values["acl"][resource]) {
- return !!this.values["acl"][resource][perm] || !!this.values["acl"][resource]["full_access"];
+ if (this.values["acl"][resource]["full_access"]) {
+ return true;
+ } else if (this.values["acl"][resource][perm]) {
+ return true;
+ }
}
return false;
}
+ // allowAny checks whether the current user is granted any of the permissions for the specified resource.
+ allowAny(resource, perms) {
+ if (this.values["acl"] && this.values["acl"][resource]) {
+ if (this.values["acl"][resource]["full_access"]) {
+ return true;
+ }
+ for (const perm of perms) {
+ if (this.values["acl"][resource][perm]) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ // deny checks whether the current user must be denied access to the specified resource.
deny(resource, perm) {
return !this.allow(resource, perm);
}
+ // denyAll checks whether the current user is granted none of the permissions for the specified resource.
+ denyAll(resource, perm) {
+ return !this.allowAny(resource, perm);
+ }
+
settings() {
return this.values.settings;
}
diff --git a/frontend/src/common/session.js b/frontend/src/common/session.js
index 8b5dcbb58..4e20a7bb5 100644
--- a/frontend/src/common/session.js
+++ b/frontend/src/common/session.js
@@ -77,7 +77,9 @@ export default class Session {
});
// Say hello.
- this.sendClientInfo();
+ this.refresh().then(() => {
+ this.sendClientInfo();
+ });
}
useSessionStorage() {
@@ -253,6 +255,27 @@ export default class Session {
});
}
+ refresh() {
+ if (this.hasId()) {
+ return Api.get("session/" + this.getId())
+ .then((resp) => {
+ if (resp.data && resp.data.config) {
+ this.setConfig(resp.data.config);
+ this.setUser(resp.data.user);
+ this.setData(resp.data.data);
+ }
+ return Promise.resolve();
+ })
+ .catch(() => {
+ this.deleteId();
+ window.location.reload();
+ return Promise.reject();
+ });
+ } else {
+ return Promise.resolve();
+ }
+ }
+
redeemToken(token) {
return Api.post("session", { token }).then((resp) => {
this.setConfig(resp.data.config);
diff --git a/frontend/src/common/util.js b/frontend/src/common/util.js
index 860053fac..b7acf9551 100644
--- a/frontend/src/common/util.js
+++ b/frontend/src/common/util.js
@@ -153,6 +153,10 @@ export default class Util {
return s.replace(/\w\S*/g, (w) => w.replace(/^\w/, (c) => c.toUpperCase()));
}
+ static generateToken() {
+ return (Math.random() + 1).toString(36).substring(6);
+ }
+
static fileType(value) {
if (!value || typeof value !== "string") {
return "";
diff --git a/frontend/src/component/album/toolbar.vue b/frontend/src/component/album/toolbar.vue
index 3f6a31c48..a7be7e01d 100644
--- a/frontend/src/component/album/toolbar.vue
+++ b/frontend/src/component/album/toolbar.vue
@@ -111,7 +111,7 @@ export default {
}].concat(this.$config.get('countries'));
const features = this.$config.settings().features;
return {
- canUpload: this.$config.allow("albums", "upload") && features.upload,
+ canUpload: this.$config.allow("files", "upload") && features.upload,
canDownload: this.$config.allow("albums", "download") && features.download,
canShare: this.$config.allow("albums", "share") && features.share,
canManage: this.$config.allow("albums", "manage"),
diff --git a/frontend/src/component/label/clipboard.vue b/frontend/src/component/label/clipboard.vue
index f06ca5b78..206113519 100644
--- a/frontend/src/component/label/clipboard.vue
+++ b/frontend/src/component/label/clipboard.vue
@@ -36,11 +36,10 @@
cloud_download
@@ -50,7 +49,7 @@
fab dark small
color="remove"
:title="$gettext('Delete')"
- :disabled="selection.length === 0"
+ :disabled="!canManage || selection.length === 0"
class="action-delete"
@click.stop="dialog.delete = true"
>
@@ -85,11 +84,20 @@ export default {
type: Array,
default: () => [],
},
- refresh: Function,
- clearSelection: Function,
+ refresh: {
+ type: Function,
+ default: () => {},
+ },
+ clearSelection: {
+ type: Function,
+ default: () => {},
+ },
},
data() {
return {
+ canManage: this.$config.allow("labels", "manage"),
+ canDownload: this.$config.allow("labels", "download"),
+ canAddAlbums: this.$config.allow("albums", "create") && this.$config.feature("albums"),
expanded: false,
dialog: {
delete: false,
@@ -105,6 +113,10 @@ export default {
this.expanded = false;
},
addToAlbum(ppid) {
+ if (!this.canAddAlbums) {
+ return;
+ }
+
this.dialog.album = false;
Api.post(`albums/${ppid}/photos`, {"labels": this.selection}).then(() => this.onAdded());
@@ -113,6 +125,10 @@ export default {
this.clearClipboard();
},
batchDelete() {
+ if (!this.canManage) {
+ return;
+ }
+
this.dialog.delete = false;
Api.post("batch/labels/delete", {"labels": this.selection}).then(this.onDeleted.bind(this));
@@ -122,6 +138,10 @@ export default {
this.clearClipboard();
},
download() {
+ if (!this.canDownload) {
+ return;
+ }
+
if (this.selection.length !== 1) {
Notify.error(this.$gettext("You can only download one label"));
return;
diff --git a/frontend/src/component/navigation.vue b/frontend/src/component/navigation.vue
index c943c27a5..eb222fed8 100644
--- a/frontend/src/component/navigation.vue
+++ b/frontend/src/component/navigation.vue
@@ -70,7 +70,7 @@