Auth: Add dummy LDAP service #98
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
d8712b4636
commit
cc38922cbe
36 changed files with 587 additions and 337 deletions
|
@ -19,8 +19,8 @@ services:
|
|||
- "~/.cache/go-mod:/go/pkg/mod"
|
||||
environment:
|
||||
PHOTOPRISM_INIT: "https"
|
||||
PHOTOPRISM_ADMIN_USER: "admin" # admin username
|
||||
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial admin password (minimum 8 characters)
|
||||
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
|
||||
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial superadmin password (minimum 8 characters)
|
||||
PHOTOPRISM_AUTH_MODE: "public" # authentication mode (public, password)
|
||||
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/"
|
||||
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
|
||||
|
|
|
@ -24,8 +24,8 @@ services:
|
|||
PHOTOPRISM_INIT: "https"
|
||||
PHOTOPRISM_UID: ${UID:-1000} # user id, should match your host user id
|
||||
PHOTOPRISM_GID: ${GID:-1000} # group id
|
||||
PHOTOPRISM_ADMIN_USER: "admin" # admin username
|
||||
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial admin password (minimum 8 characters)
|
||||
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
|
||||
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial superadmin password (minimum 8 characters)
|
||||
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
|
||||
PHOTOPRISM_SITE_URL: "https://latest.localssl.dev/" # server URL in the format "http(s)://domain.name(:port)/(path)"
|
||||
PHOTOPRISM_SITE_CAPTION: "Latest"
|
||||
|
|
|
@ -24,8 +24,8 @@ services:
|
|||
environment:
|
||||
PHOTOPRISM_UID: ${UID:-1000} # user id, should match your host user id
|
||||
PHOTOPRISM_GID: ${GID:-1000} # group id
|
||||
PHOTOPRISM_ADMIN_USER: "admin" # admin username
|
||||
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial admin password (minimum 8 characters)
|
||||
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
|
||||
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial superadmin password (minimum 8 characters)
|
||||
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
|
||||
PHOTOPRISM_SITE_URL: "https://latest.localssl.dev/" # server URL in the format "http(s)://domain.name(:port)/(path)"
|
||||
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
|
||||
|
|
|
@ -27,8 +27,8 @@ services:
|
|||
shm_size: "2gb"
|
||||
environment:
|
||||
PHOTOPRISM_INIT: "https"
|
||||
PHOTOPRISM_ADMIN_USER: "admin" # admin username
|
||||
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial admin password (minimum 8 characters)
|
||||
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
|
||||
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial superadmin password (minimum 8 characters)
|
||||
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
|
||||
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/"
|
||||
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
|
||||
|
|
|
@ -24,8 +24,8 @@ services:
|
|||
- "traefik:localssl.dev"
|
||||
- "traefik:app.localssl.dev"
|
||||
- "traefik:keycloak.localssl.dev"
|
||||
- "traefik:dummy-webdav.localssl.dev"
|
||||
- "traefik:dummy-oidc.localssl.dev"
|
||||
- "traefik:dummy-webdav.localssl.dev"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.services.photoprism.loadbalancer.server.port=2342"
|
||||
|
@ -39,9 +39,26 @@ services:
|
|||
## Run as a non-root user after initialization (supported: 0, 33, 50-99, 500-600, and 900-1200):
|
||||
PHOTOPRISM_UID: ${UID:-1000} # user id, should match your host user id
|
||||
PHOTOPRISM_GID: ${GID:-1000} # group id
|
||||
PHOTOPRISM_ADMIN_USER: "admin" # admin username
|
||||
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial admin password (minimum 8 characters)
|
||||
## Access Management
|
||||
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
|
||||
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial superadmin password (minimum 8 characters)
|
||||
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
|
||||
PHOTOPRISM_REGISTER_URI: "https://keycloak.localssl.dev/admin/"
|
||||
PHOTOPRISM_PASSWORD_RESET_URI: "https://keycloak.localssl.dev/realms/master/login-actions/reset-credentials"
|
||||
## LDAP Authentication (pre-configured for local tests):
|
||||
PHOTOPRISM_LDAP_URI: "ldaps://dummy-ldap:1636"
|
||||
PHOTOPRISM_LDAP_INSECURE: "true"
|
||||
PHOTOPRISM_LDAP_ROLE: "user"
|
||||
PHOTOPRISM_LDAP_WEBDAV: "true"
|
||||
PHOTOPRISM_LDAP_BIND: "simple"
|
||||
PHOTOPRISM_LDAP_BIND_DN: "cn"
|
||||
PHOTOPRISM_LDAP_BASE_DN: "dc=localssl,dc=dev"
|
||||
## OpenID Connect (pre-configured for local tests):
|
||||
PHOTOPRISM_OIDC_URI: "https://keycloak.localssl.dev/auth/realms/master"
|
||||
PHOTOPRISM_OIDC_INSECURE: "true"
|
||||
PHOTOPRISM_OIDC_CLIENT: "photoprism-develop"
|
||||
PHOTOPRISM_OIDC_SECRET: "9d8351a0-ca01-4556-9c37-85eb634869b9"
|
||||
## Site Information
|
||||
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/" # server URL in the format "http(s)://domain.name(:port)/(path)"
|
||||
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
|
||||
PHOTOPRISM_SITE_DESCRIPTION: "Tags and finds pictures without getting in your way!"
|
||||
|
@ -83,10 +100,6 @@ services:
|
|||
PHOTOPRISM_JPEG_SIZE: 7680 # size limit for converted image files in pixels (720-30000)
|
||||
PHOTOPRISM_JPEG_QUALITY: 85 # a higher value increases the quality and file size of JPEG images and thumbnails (25-100)
|
||||
TF_CPP_MIN_LOG_LEVEL: 0 # show TensorFlow log messages for development
|
||||
## OpenID Connect Provider (pre-configured for local Keycloak test server):
|
||||
PHOTOPRISM_OIDC_ISSUER_URL: "https://keycloak.localssl.dev/auth/realms/master"
|
||||
PHOTOPRISM_OIDC_CLIENT_ID: "photoprism-develop"
|
||||
PHOTOPRISM_OIDC_CLIENT_SECRET: "9d8351a0-ca01-4556-9c37-85eb634869b9"
|
||||
## Run/install on first startup (options: update https gpu tensorflow davfs clitools clean):
|
||||
PHOTOPRISM_INIT: "https tensorflow"
|
||||
## Hardware Video Transcoding (optional):
|
||||
|
@ -151,6 +164,7 @@ services:
|
|||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:19.0
|
||||
command: "start-dev" # development mode, do not use this in production!
|
||||
container_name: keycloak
|
||||
links:
|
||||
- "traefik:localssl.dev"
|
||||
- "traefik:app.localssl.dev"
|
||||
|
@ -174,9 +188,32 @@ services:
|
|||
KC_DB_USERNAME: "keycloak"
|
||||
KC_DB_PASSWORD: "keycloak"
|
||||
|
||||
## Dummy LDAP Server
|
||||
dummy-ldap:
|
||||
image: openidentityplatform/opendj:latest
|
||||
container_name: dummy-ldap
|
||||
expose:
|
||||
- 1389
|
||||
- 1636
|
||||
- 4444
|
||||
# ports:
|
||||
# - "1389:1389"
|
||||
# - "1636:1636"
|
||||
# - "4444:4444"
|
||||
user: "1001:1000"
|
||||
environment:
|
||||
OPENDJ_USER: 1001
|
||||
PORT: 1389
|
||||
LDAPS_PORT: 1636
|
||||
BASE_DN: "dc=localssl,dc=dev"
|
||||
ADD_BASE_ENTRY: "--addBaseEntry"
|
||||
ROOT_USER_DN: "cn=user"
|
||||
ROOT_PASSWORD: "photoprism"
|
||||
|
||||
## Dummy OpenID Connect Provider
|
||||
dummy-oidc:
|
||||
image: photoprism/dummy-oidc:220405
|
||||
container_name: dummy-oidc
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.services.dummy-oidc.loadbalancer.server.port=9998"
|
||||
|
@ -189,6 +226,7 @@ services:
|
|||
## Dummy WebDAV Server
|
||||
dummy-webdav:
|
||||
image: photoprism/dummy-webdav:220405
|
||||
container_name: dummy-webdav
|
||||
environment:
|
||||
WEBDAV_USERNAME: admin
|
||||
WEBDAV_PASSWORD: photoprism
|
||||
|
|
|
@ -61,6 +61,7 @@ export default class Config {
|
|||
this.themeName = "";
|
||||
this.baseUri = "";
|
||||
this.staticUri = "/static";
|
||||
this.loginUri = "/library/login";
|
||||
this.apiUri = "/api/v1";
|
||||
this.contentUri = this.apiUri;
|
||||
this.values = {
|
||||
|
@ -75,6 +76,7 @@ export default class Config {
|
|||
} else {
|
||||
this.baseUri = values.baseUri ? values.baseUri : "";
|
||||
this.staticUri = values.staticUri ? values.staticUri : this.baseUri + "/static";
|
||||
this.loginUri = values.loginUri ? values.loginUri : this.baseUri + "/library/login";
|
||||
this.apiUri = values.apiUri ? values.apiUri : this.baseUri + "/api/v1";
|
||||
this.contentUri = values.contentUri ? values.contentUri : this.apiUri;
|
||||
}
|
||||
|
|
|
@ -142,6 +142,7 @@ export default class Session {
|
|||
|
||||
deleteId() {
|
||||
this.session_id = null;
|
||||
this.provider = "";
|
||||
this.storage.removeItem("session_id");
|
||||
|
||||
delete Api.defaults.headers.common[SessionHeader];
|
||||
|
@ -157,6 +158,9 @@ export default class Session {
|
|||
if (resp.data.id) {
|
||||
this.setId(resp.data.id);
|
||||
}
|
||||
if (resp.data.provider) {
|
||||
this.provider = resp.data.provider;
|
||||
}
|
||||
if (resp.data.config) {
|
||||
this.setConfig(resp.data.config);
|
||||
}
|
||||
|
|
|
@ -44,6 +44,30 @@
|
|||
max-width: 1264px;
|
||||
}
|
||||
|
||||
.width-50 {
|
||||
min-width: 50%;
|
||||
}
|
||||
|
||||
.width-60 {
|
||||
min-width: 60%;
|
||||
}
|
||||
|
||||
.width-66 {
|
||||
min-width: 66%;
|
||||
}
|
||||
|
||||
.width-70 {
|
||||
min-width: 70%;
|
||||
}
|
||||
|
||||
.width-80 {
|
||||
min-width: 80%;
|
||||
}
|
||||
|
||||
.width-90 {
|
||||
min-width: 90%;
|
||||
}
|
||||
|
||||
/* Rounded Elements */
|
||||
|
||||
.v-progress-linear,
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
:label="$gettext('New Password')"
|
||||
class="input-new-password"
|
||||
color="secondary-dark"
|
||||
:hint="$gettext('Must have at least 8 characters.')"
|
||||
:hint="$gettextInterpolate($gettext('Must have at least %{n} characters.'), {n: passwordLength})"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
|
||||
|
@ -101,6 +101,7 @@ export default {
|
|||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
passwordLength: this.$config.get("passwordLength"),
|
||||
rtl: this.$rtl,
|
||||
};
|
||||
},
|
||||
|
@ -112,7 +113,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
disabled() {
|
||||
return (this.isDemo || this.busy || this.oldPassword === "" || this.newPassword.length < 8 || (this.newPassword !== this.confirmPassword));
|
||||
return (this.isDemo || this.busy || this.oldPassword === "" || this.newPassword.length < this.passwordLength || (this.newPassword !== this.confirmPassword));
|
||||
},
|
||||
confirm() {
|
||||
this.busy = true;
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
:disabled="disabled"
|
||||
:rules="[textRule]"
|
||||
hide-details box flat
|
||||
:label="$gettext('Title')"
|
||||
:label="$pgettext('Photo', 'Title')"
|
||||
placeholder=""
|
||||
color="secondary-dark"
|
||||
browser-autocomplete="off"
|
||||
|
|
|
@ -45,13 +45,19 @@
|
|||
></v-text-field>
|
||||
<v-spacer></v-spacer>
|
||||
<div class="action-buttons text-xs-center">
|
||||
<!-- a href="#" target="_blank" class="text-link px-2" :style="`color: ${colors.link}!important`"><translate>Forgot password?</translate></a -->
|
||||
<v-btn :color="colors.primary" depressed :disabled="loginDisabled"
|
||||
class="white--text action-confirm ra-6 px-3" @click.stop="login">
|
||||
<v-btn v-if="registerUri" :color="colors.secondary" outline :block="$vuetify.breakpoint.xsOnly"
|
||||
:style="`color: ${colors.link}!important`" class="action-register ra-6 px-3 py-2 opacity-80" @click.stop="register">
|
||||
<translate>Create Account</translate>
|
||||
</v-btn>
|
||||
<v-btn :color="colors.primary" depressed :disabled="loginDisabled" :block="$vuetify.breakpoint.xsOnly"
|
||||
class="white--text action-confirm ra-6 py-2 px-3" @click.stop="login">
|
||||
<translate>Sign in</translate>
|
||||
<v-icon :right="!rtl" :left="rtl" dark>arrow_forward</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div v-if="passwordResetUri" class="text-xs-center opacity-80">
|
||||
<a :href="passwordResetUri" class="text-link" :style="`color: ${colors.link}!important`"><translate>Forgot Password?</translate></a>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-form>
|
||||
|
@ -103,7 +109,8 @@ export default {
|
|||
return {
|
||||
colors: {
|
||||
accent: "#05dde1",
|
||||
primary: "#00adb0",
|
||||
primary: "#00a6a9",
|
||||
secondary: "#505050",
|
||||
link: "#c8e3e7",
|
||||
},
|
||||
loading: false,
|
||||
|
@ -115,6 +122,8 @@ export default {
|
|||
siteDescription: this.$config.getSiteDescription(),
|
||||
nextUrl: this.$route.params.nextUrl ? this.$route.params.nextUrl : "/",
|
||||
wallpaperUri: this.$config.values.wallpaperUri,
|
||||
registerUri: this.$config.values.registerUri,
|
||||
passwordResetUri: this.$config.values.passwordResetUri,
|
||||
rtl: this.$rtl,
|
||||
};
|
||||
},
|
||||
|
@ -146,6 +155,9 @@ export default {
|
|||
|
||||
setTimeout(() => { window.location = route.href; }, 100);
|
||||
},
|
||||
register() {
|
||||
window.location = this.registerUri;
|
||||
},
|
||||
login() {
|
||||
const username = this.username.trim();
|
||||
const password = this.password.trim();
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Title')"
|
||||
:label="$pgettext('Account', 'Title')"
|
||||
class="input-name-title"
|
||||
color="secondary-dark"
|
||||
:rules="[v => validLength(v, 0, 32) || $gettext('Invalid')]"
|
||||
|
|
4
go.mod
4
go.mod
|
@ -97,6 +97,8 @@ require (
|
|||
golang.org/x/time v0.2.0
|
||||
)
|
||||
|
||||
require github.com/go-ldap/ldap/v3 v3.4.4
|
||||
|
||||
require (
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
|
||||
|
@ -105,6 +107,7 @@ require (
|
|||
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
|
@ -112,6 +115,7 @@ require (
|
|||
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||
github.com/go-errors/errors v1.4.2 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
|
|
8
go.sum
8
go.sum
|
@ -202,6 +202,8 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z
|
|||
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
|
@ -382,6 +384,8 @@ github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR
|
|||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-acme/lego/v4 v4.9.0 h1:8Hjj44IqRS7cigshMyFQ+0pIZvwgkG/+9A0UnNh7G8A=
|
||||
github.com/go-acme/lego/v4 v4.9.0/go.mod h1:g3JRUyWS3L/VObpp4bCxzJftKyf/Wba8QrSSnoiqjg4=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
|
||||
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
|
@ -405,6 +409,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
|
|||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
|
||||
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
|
||||
github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs=
|
||||
github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
|
@ -918,6 +924,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
|
|||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
|
@ -1022,6 +1029,7 @@ golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0
|
|||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
|
|
|
@ -32,11 +32,12 @@ func CreateSession(router *gin.RouterGroup) {
|
|||
if conf.Public() {
|
||||
sess := get.Session().Public()
|
||||
data := gin.H{
|
||||
"status": "ok",
|
||||
"id": sess.ID,
|
||||
"user": sess.User(),
|
||||
"data": sess.Data(),
|
||||
"config": conf.ClientPublic(),
|
||||
"status": "ok",
|
||||
"id": sess.ID,
|
||||
"provider": sess.AuthProvider,
|
||||
"user": sess.User(),
|
||||
"data": sess.Data(),
|
||||
"config": conf.ClientPublic(),
|
||||
}
|
||||
c.JSON(http.StatusOK, data)
|
||||
return
|
||||
|
@ -86,11 +87,12 @@ func CreateSession(router *gin.RouterGroup) {
|
|||
|
||||
// User information, session data, and client config values.
|
||||
data := gin.H{
|
||||
"status": "ok",
|
||||
"id": sess.ID,
|
||||
"user": sess.User(),
|
||||
"data": sess.Data(),
|
||||
"config": clientConfig,
|
||||
"status": "ok",
|
||||
"id": sess.ID,
|
||||
"provider": sess.AuthProvider,
|
||||
"user": sess.User(),
|
||||
"data": sess.Data(),
|
||||
"config": clientConfig,
|
||||
}
|
||||
|
||||
// Send JSON response.
|
||||
|
|
|
@ -55,11 +55,12 @@ func GetSession(router *gin.RouterGroup) {
|
|||
|
||||
// Send JSON response with user information, session data, and client config values.
|
||||
data := gin.H{
|
||||
"status": "ok",
|
||||
"id": sess.ID,
|
||||
"user": sess.User(),
|
||||
"data": sess.Data(),
|
||||
"config": get.Config().ClientSession(sess),
|
||||
"status": "ok",
|
||||
"id": sess.ID,
|
||||
"provider": sess.AuthProvider,
|
||||
"user": sess.User(),
|
||||
"data": sess.Data(),
|
||||
"config": get.Config().ClientSession(sess),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
|
|
|
@ -58,8 +58,8 @@ func SaveSettings(router *gin.RouterGroup) {
|
|||
|
||||
var settings *customize.Settings
|
||||
|
||||
if s.User().IsAdmin() {
|
||||
// Only admins may change the global config.
|
||||
// Only super admins can change global config defaults.
|
||||
if s.User().IsSuperAdmin() {
|
||||
settings = conf.Settings()
|
||||
|
||||
if err := c.BindJSON(settings); err != nil {
|
||||
|
|
|
@ -64,10 +64,10 @@ func usersAddAction(ctx *cli.Context) error {
|
|||
frm.UserEmail = clean.Email(res)
|
||||
}
|
||||
|
||||
if interactive && len(ctx.String("password")) < entity.LenPasswordMin {
|
||||
if interactive && len(ctx.String("password")) < entity.PasswordLength {
|
||||
validate := func(input string) error {
|
||||
if len(input) < entity.LenPasswordMin {
|
||||
return fmt.Errorf("password must have at least %d characters", entity.LenPasswordMin)
|
||||
if len(input) < entity.PasswordLength {
|
||||
return fmt.Errorf("password must have at least %d characters", entity.PasswordLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ var UsersListCommand = cli.Command{
|
|||
// usersListAction displays existing user accounts.
|
||||
func usersListAction(ctx *cli.Context) error {
|
||||
return CallWithDependencies(ctx, func(conf *config.Config) error {
|
||||
cols := []string{"ID", "UID", "User Name", "Display Name", "Email", "Role", "Super Admin", "Web Login", "WebDAV", "Attributes", "Created At"}
|
||||
cols := []string{"User", "Login", "Full Name", "Email", "Role", "Super Admin", "Web UI", "WebDAV", "Attributes", "Created At"}
|
||||
|
||||
// Fetch users from database.
|
||||
users := query.RegisteredUsers()
|
||||
|
@ -35,9 +35,8 @@ func usersListAction(ctx *cli.Context) error {
|
|||
// Display report.
|
||||
for i, user := range users {
|
||||
rows[i] = []string{
|
||||
fmt.Sprintf("%d", user.ID),
|
||||
user.UID(),
|
||||
user.Name(),
|
||||
user.Login(),
|
||||
user.FullName(),
|
||||
user.Email(),
|
||||
user.AclRole().String(),
|
||||
|
|
|
@ -23,64 +23,68 @@ const (
|
|||
|
||||
// ClientConfig represents HTTP client / Web UI config options.
|
||||
type ClientConfig struct {
|
||||
Mode string `json:"mode"`
|
||||
Name string `json:"name"`
|
||||
About string `json:"about"`
|
||||
Edition string `json:"edition"`
|
||||
Version string `json:"version"`
|
||||
Copyright string `json:"copyright"`
|
||||
Flags string `json:"flags"`
|
||||
BaseUri string `json:"baseUri"`
|
||||
StaticUri string `json:"staticUri"`
|
||||
CssUri string `json:"cssUri"`
|
||||
JsUri string `json:"jsUri"`
|
||||
ManifestUri string `json:"manifestUri"`
|
||||
ApiUri string `json:"apiUri"`
|
||||
ContentUri string `json:"contentUri"`
|
||||
WallpaperUri string `json:"wallpaperUri"`
|
||||
SiteUrl string `json:"siteUrl"`
|
||||
SiteDomain string `json:"siteDomain"`
|
||||
SiteAuthor string `json:"siteAuthor"`
|
||||
SiteTitle string `json:"siteTitle"`
|
||||
SiteCaption string `json:"siteCaption"`
|
||||
SiteDescription string `json:"siteDescription"`
|
||||
SitePreview string `json:"sitePreview"`
|
||||
LegalInfo string `json:"legalInfo"`
|
||||
LegalUrl string `json:"legalUrl"`
|
||||
AppName string `json:"appName"`
|
||||
AppMode string `json:"appMode"`
|
||||
AppIcon string `json:"appIcon"`
|
||||
Debug bool `json:"debug"`
|
||||
Trace bool `json:"trace"`
|
||||
Test bool `json:"test"`
|
||||
Demo bool `json:"demo"`
|
||||
Sponsor bool `json:"sponsor"`
|
||||
ReadOnly bool `json:"readonly"`
|
||||
UploadNSFW bool `json:"uploadNSFW"`
|
||||
Public bool `json:"public"`
|
||||
AuthMode string `json:"authMode"`
|
||||
Experimental bool `json:"experimental"`
|
||||
AlbumCategories []string `json:"albumCategories"`
|
||||
Albums entity.Albums `json:"albums"`
|
||||
Cameras entity.Cameras `json:"cameras"`
|
||||
Lenses entity.Lenses `json:"lenses"`
|
||||
Countries entity.Countries `json:"countries"`
|
||||
People entity.People `json:"people"`
|
||||
Thumbs ThumbSizes `json:"thumbs"`
|
||||
MapKey string `json:"mapKey"`
|
||||
DownloadToken string `json:"downloadToken,omitempty"`
|
||||
PreviewToken string `json:"previewToken,omitempty"`
|
||||
Disable ClientDisable `json:"disable"`
|
||||
Count ClientCounts `json:"count"`
|
||||
Pos ClientPosition `json:"pos"`
|
||||
Years Years `json:"years"`
|
||||
Colors []map[string]string `json:"colors"`
|
||||
Categories CategoryLabels `json:"categories"`
|
||||
Clip int `json:"clip"`
|
||||
Server env.Resources `json:"server"`
|
||||
Settings *customize.Settings `json:"settings,omitempty"`
|
||||
ACL acl.Grants `json:"acl,omitempty"`
|
||||
Ext Values `json:"ext"`
|
||||
Mode string `json:"mode"`
|
||||
Name string `json:"name"`
|
||||
About string `json:"about"`
|
||||
Edition string `json:"edition"`
|
||||
Version string `json:"version"`
|
||||
Copyright string `json:"copyright"`
|
||||
Flags string `json:"flags"`
|
||||
BaseUri string `json:"baseUri"`
|
||||
StaticUri string `json:"staticUri"`
|
||||
CssUri string `json:"cssUri"`
|
||||
JsUri string `json:"jsUri"`
|
||||
ManifestUri string `json:"manifestUri"`
|
||||
ApiUri string `json:"apiUri"`
|
||||
ContentUri string `json:"contentUri"`
|
||||
WallpaperUri string `json:"wallpaperUri"`
|
||||
SiteUrl string `json:"siteUrl"`
|
||||
SiteDomain string `json:"siteDomain"`
|
||||
SiteAuthor string `json:"siteAuthor"`
|
||||
SiteTitle string `json:"siteTitle"`
|
||||
SiteCaption string `json:"siteCaption"`
|
||||
SiteDescription string `json:"siteDescription"`
|
||||
SitePreview string `json:"sitePreview"`
|
||||
LegalInfo string `json:"legalInfo"`
|
||||
LegalUrl string `json:"legalUrl"`
|
||||
AppName string `json:"appName"`
|
||||
AppMode string `json:"appMode"`
|
||||
AppIcon string `json:"appIcon"`
|
||||
Debug bool `json:"debug"`
|
||||
Trace bool `json:"trace"`
|
||||
Test bool `json:"test"`
|
||||
Demo bool `json:"demo"`
|
||||
Sponsor bool `json:"sponsor"`
|
||||
ReadOnly bool `json:"readonly"`
|
||||
UploadNSFW bool `json:"uploadNSFW"`
|
||||
Public bool `json:"public"`
|
||||
AuthMode string `json:"authMode"`
|
||||
LoginUri string `json:"loginUri"`
|
||||
RegisterUri string `json:"registerUri"`
|
||||
PasswordLength int `json:"passwordLength"`
|
||||
PasswordResetUri string `json:"passwordResetUri"`
|
||||
Experimental bool `json:"experimental"`
|
||||
AlbumCategories []string `json:"albumCategories"`
|
||||
Albums entity.Albums `json:"albums"`
|
||||
Cameras entity.Cameras `json:"cameras"`
|
||||
Lenses entity.Lenses `json:"lenses"`
|
||||
Countries entity.Countries `json:"countries"`
|
||||
People entity.People `json:"people"`
|
||||
Thumbs ThumbSizes `json:"thumbs"`
|
||||
MapKey string `json:"mapKey"`
|
||||
DownloadToken string `json:"downloadToken,omitempty"`
|
||||
PreviewToken string `json:"previewToken,omitempty"`
|
||||
Disable ClientDisable `json:"disable"`
|
||||
Count ClientCounts `json:"count"`
|
||||
Pos ClientPosition `json:"pos"`
|
||||
Years Years `json:"years"`
|
||||
Colors []map[string]string `json:"colors"`
|
||||
Categories CategoryLabels `json:"categories"`
|
||||
Clip int `json:"clip"`
|
||||
Server env.Resources `json:"server"`
|
||||
Settings *customize.Settings `json:"settings,omitempty"`
|
||||
ACL acl.Grants `json:"acl,omitempty"`
|
||||
Ext Values `json:"ext"`
|
||||
}
|
||||
|
||||
// ApplyACL updates the client config values based on the ACL and Role provided.
|
||||
|
@ -225,54 +229,57 @@ func (c *Config) ClientPublic() ClientConfig {
|
|||
Faces: true,
|
||||
Classification: true,
|
||||
},
|
||||
Flags: strings.Join(c.Flags(), " "),
|
||||
Mode: string(ClientPublic),
|
||||
Name: c.Name(),
|
||||
About: c.Edition(),
|
||||
Edition: c.Hub().Status,
|
||||
BaseUri: c.BaseUri(""),
|
||||
StaticUri: c.StaticUri(),
|
||||
CssUri: a.AppCssUri(),
|
||||
JsUri: a.AppJsUri(),
|
||||
ApiUri: c.ApiUri(),
|
||||
ContentUri: c.ContentUri(),
|
||||
SiteUrl: c.SiteUrl(),
|
||||
SiteDomain: c.SiteDomain(),
|
||||
SiteAuthor: c.SiteAuthor(),
|
||||
SiteTitle: c.SiteTitle(),
|
||||
SiteCaption: c.SiteCaption(),
|
||||
SiteDescription: c.SiteDescription(),
|
||||
SitePreview: c.SitePreview(),
|
||||
LegalInfo: c.LegalInfo(),
|
||||
LegalUrl: c.LegalUrl(),
|
||||
AppName: c.AppName(),
|
||||
AppMode: c.AppMode(),
|
||||
AppIcon: c.AppIcon(),
|
||||
WallpaperUri: c.WallpaperUri(),
|
||||
Version: c.Version(),
|
||||
Copyright: c.Copyright(),
|
||||
Debug: c.Debug(),
|
||||
Trace: c.Trace(),
|
||||
Test: c.Test(),
|
||||
Demo: c.Demo(),
|
||||
Sponsor: c.Sponsor(),
|
||||
ReadOnly: c.ReadOnly(),
|
||||
Public: c.Public(),
|
||||
AuthMode: c.AuthMode(),
|
||||
Experimental: c.Experimental(),
|
||||
Albums: entity.Albums{},
|
||||
Cameras: entity.Cameras{},
|
||||
Lenses: entity.Lenses{},
|
||||
Countries: entity.Countries{},
|
||||
People: entity.People{},
|
||||
MapKey: "",
|
||||
Thumbs: Thumbs,
|
||||
Colors: colors.All.List(),
|
||||
ManifestUri: c.ClientManifestUri(),
|
||||
Clip: txt.ClipDefault,
|
||||
PreviewToken: entity.TokenPublic,
|
||||
DownloadToken: entity.TokenPublic,
|
||||
Ext: ClientExt(c, ClientPublic),
|
||||
Flags: strings.Join(c.Flags(), " "),
|
||||
Mode: string(ClientPublic),
|
||||
Name: c.Name(),
|
||||
About: c.Edition(),
|
||||
Edition: c.Hub().Status,
|
||||
BaseUri: c.BaseUri(""),
|
||||
StaticUri: c.StaticUri(),
|
||||
CssUri: a.AppCssUri(),
|
||||
JsUri: a.AppJsUri(),
|
||||
ApiUri: c.ApiUri(),
|
||||
ContentUri: c.ContentUri(),
|
||||
SiteUrl: c.SiteUrl(),
|
||||
SiteDomain: c.SiteDomain(),
|
||||
SiteAuthor: c.SiteAuthor(),
|
||||
SiteTitle: c.SiteTitle(),
|
||||
SiteCaption: c.SiteCaption(),
|
||||
SiteDescription: c.SiteDescription(),
|
||||
SitePreview: c.SitePreview(),
|
||||
LegalInfo: c.LegalInfo(),
|
||||
LegalUrl: c.LegalUrl(),
|
||||
AppName: c.AppName(),
|
||||
AppMode: c.AppMode(),
|
||||
AppIcon: c.AppIcon(),
|
||||
WallpaperUri: c.WallpaperUri(),
|
||||
Version: c.Version(),
|
||||
Copyright: c.Copyright(),
|
||||
Debug: c.Debug(),
|
||||
Trace: c.Trace(),
|
||||
Test: c.Test(),
|
||||
Demo: c.Demo(),
|
||||
Sponsor: c.Sponsor(),
|
||||
ReadOnly: c.ReadOnly(),
|
||||
Public: c.Public(),
|
||||
AuthMode: c.AuthMode(),
|
||||
LoginUri: c.LoginUri(),
|
||||
RegisterUri: c.RegisterUri(),
|
||||
PasswordResetUri: c.PasswordResetUri(),
|
||||
Experimental: c.Experimental(),
|
||||
Albums: entity.Albums{},
|
||||
Cameras: entity.Cameras{},
|
||||
Lenses: entity.Lenses{},
|
||||
Countries: entity.Countries{},
|
||||
People: entity.People{},
|
||||
MapKey: "",
|
||||
Thumbs: Thumbs,
|
||||
Colors: colors.All.List(),
|
||||
ManifestUri: c.ClientManifestUri(),
|
||||
Clip: txt.ClipDefault,
|
||||
PreviewToken: entity.TokenPublic,
|
||||
DownloadToken: entity.TokenPublic,
|
||||
Ext: ClientExt(c, ClientPublic),
|
||||
}
|
||||
|
||||
return cfg
|
||||
|
@ -301,55 +308,58 @@ func (c *Config) ClientShare() ClientConfig {
|
|||
Faces: true,
|
||||
Classification: true,
|
||||
},
|
||||
Flags: strings.Join(c.Flags(), " "),
|
||||
Mode: string(ClientShare),
|
||||
Name: c.Name(),
|
||||
About: c.Edition(),
|
||||
Edition: c.Hub().Status,
|
||||
BaseUri: c.BaseUri(""),
|
||||
StaticUri: c.StaticUri(),
|
||||
CssUri: a.AppCssUri(),
|
||||
JsUri: a.ShareJsUri(),
|
||||
ApiUri: c.ApiUri(),
|
||||
ContentUri: c.ContentUri(),
|
||||
SiteUrl: c.SiteUrl(),
|
||||
SiteDomain: c.SiteDomain(),
|
||||
SiteAuthor: c.SiteAuthor(),
|
||||
SiteTitle: c.SiteTitle(),
|
||||
SiteCaption: c.SiteCaption(),
|
||||
SiteDescription: c.SiteDescription(),
|
||||
SitePreview: c.SitePreview(),
|
||||
LegalInfo: c.LegalInfo(),
|
||||
LegalUrl: c.LegalUrl(),
|
||||
AppName: c.AppName(),
|
||||
AppMode: c.AppMode(),
|
||||
AppIcon: c.AppIcon(),
|
||||
WallpaperUri: c.WallpaperUri(),
|
||||
Version: c.Version(),
|
||||
Copyright: c.Copyright(),
|
||||
Debug: c.Debug(),
|
||||
Trace: c.Trace(),
|
||||
Test: c.Test(),
|
||||
Demo: c.Demo(),
|
||||
Sponsor: c.Sponsor(),
|
||||
ReadOnly: c.ReadOnly(),
|
||||
UploadNSFW: c.UploadNSFW(),
|
||||
Public: c.Public(),
|
||||
AuthMode: c.AuthMode(),
|
||||
Experimental: c.Experimental(),
|
||||
Albums: entity.Albums{},
|
||||
Cameras: entity.Cameras{},
|
||||
Lenses: entity.Lenses{},
|
||||
Countries: entity.Countries{},
|
||||
People: entity.People{},
|
||||
Colors: colors.All.List(),
|
||||
Thumbs: Thumbs,
|
||||
MapKey: c.Hub().MapKey(),
|
||||
DownloadToken: c.DownloadToken(),
|
||||
PreviewToken: c.PreviewToken(),
|
||||
ManifestUri: c.ClientManifestUri(),
|
||||
Clip: txt.ClipDefault,
|
||||
Ext: ClientExt(c, ClientShare),
|
||||
Flags: strings.Join(c.Flags(), " "),
|
||||
Mode: string(ClientShare),
|
||||
Name: c.Name(),
|
||||
About: c.Edition(),
|
||||
Edition: c.Hub().Status,
|
||||
BaseUri: c.BaseUri(""),
|
||||
StaticUri: c.StaticUri(),
|
||||
CssUri: a.AppCssUri(),
|
||||
JsUri: a.ShareJsUri(),
|
||||
ApiUri: c.ApiUri(),
|
||||
ContentUri: c.ContentUri(),
|
||||
SiteUrl: c.SiteUrl(),
|
||||
SiteDomain: c.SiteDomain(),
|
||||
SiteAuthor: c.SiteAuthor(),
|
||||
SiteTitle: c.SiteTitle(),
|
||||
SiteCaption: c.SiteCaption(),
|
||||
SiteDescription: c.SiteDescription(),
|
||||
SitePreview: c.SitePreview(),
|
||||
LegalInfo: c.LegalInfo(),
|
||||
LegalUrl: c.LegalUrl(),
|
||||
AppName: c.AppName(),
|
||||
AppMode: c.AppMode(),
|
||||
AppIcon: c.AppIcon(),
|
||||
WallpaperUri: c.WallpaperUri(),
|
||||
Version: c.Version(),
|
||||
Copyright: c.Copyright(),
|
||||
Debug: c.Debug(),
|
||||
Trace: c.Trace(),
|
||||
Test: c.Test(),
|
||||
Demo: c.Demo(),
|
||||
Sponsor: c.Sponsor(),
|
||||
ReadOnly: c.ReadOnly(),
|
||||
UploadNSFW: c.UploadNSFW(),
|
||||
Public: c.Public(),
|
||||
AuthMode: c.AuthMode(),
|
||||
LoginUri: c.LoginUri(),
|
||||
RegisterUri: c.RegisterUri(),
|
||||
PasswordResetUri: c.PasswordResetUri(),
|
||||
Experimental: c.Experimental(),
|
||||
Albums: entity.Albums{},
|
||||
Cameras: entity.Cameras{},
|
||||
Lenses: entity.Lenses{},
|
||||
Countries: entity.Countries{},
|
||||
People: entity.People{},
|
||||
Colors: colors.All.List(),
|
||||
Thumbs: Thumbs,
|
||||
MapKey: c.Hub().MapKey(),
|
||||
DownloadToken: c.DownloadToken(),
|
||||
PreviewToken: c.PreviewToken(),
|
||||
ManifestUri: c.ClientManifestUri(),
|
||||
Clip: txt.ClipDefault,
|
||||
Ext: ClientExt(c, ClientShare),
|
||||
}
|
||||
|
||||
return cfg
|
||||
|
@ -383,56 +393,60 @@ func (c *Config) ClientUser(withSettings bool) ClientConfig {
|
|||
Faces: c.DisableFaces(),
|
||||
Classification: c.DisableClassification(),
|
||||
},
|
||||
Flags: strings.Join(c.Flags(), " "),
|
||||
Mode: string(ClientUser),
|
||||
Name: c.Name(),
|
||||
About: c.Edition(),
|
||||
Edition: c.Hub().Status,
|
||||
BaseUri: c.BaseUri(""),
|
||||
StaticUri: c.StaticUri(),
|
||||
CssUri: a.AppCssUri(),
|
||||
JsUri: a.AppJsUri(),
|
||||
ApiUri: c.ApiUri(),
|
||||
ContentUri: c.ContentUri(),
|
||||
SiteUrl: c.SiteUrl(),
|
||||
SiteDomain: c.SiteDomain(),
|
||||
SiteAuthor: c.SiteAuthor(),
|
||||
SiteTitle: c.SiteTitle(),
|
||||
SiteCaption: c.SiteCaption(),
|
||||
SiteDescription: c.SiteDescription(),
|
||||
SitePreview: c.SitePreview(),
|
||||
LegalInfo: c.LegalInfo(),
|
||||
LegalUrl: c.LegalUrl(),
|
||||
AppName: c.AppName(),
|
||||
AppMode: c.AppMode(),
|
||||
AppIcon: c.AppIcon(),
|
||||
WallpaperUri: c.WallpaperUri(),
|
||||
Version: c.Version(),
|
||||
Copyright: c.Copyright(),
|
||||
Debug: c.Debug(),
|
||||
Trace: c.Trace(),
|
||||
Test: c.Test(),
|
||||
Demo: c.Demo(),
|
||||
Sponsor: c.Sponsor(),
|
||||
ReadOnly: c.ReadOnly(),
|
||||
UploadNSFW: c.UploadNSFW(),
|
||||
Public: c.Public(),
|
||||
AuthMode: c.AuthMode(),
|
||||
Experimental: c.Experimental(),
|
||||
Albums: entity.Albums{},
|
||||
Cameras: entity.Cameras{},
|
||||
Lenses: entity.Lenses{},
|
||||
Countries: entity.Countries{},
|
||||
People: entity.People{},
|
||||
Colors: colors.All.List(),
|
||||
Thumbs: Thumbs,
|
||||
MapKey: c.Hub().MapKey(),
|
||||
DownloadToken: c.DownloadToken(),
|
||||
PreviewToken: c.PreviewToken(),
|
||||
ManifestUri: c.ClientManifestUri(),
|
||||
Clip: txt.ClipDefault,
|
||||
Server: env.Info(),
|
||||
Ext: ClientExt(c, ClientUser),
|
||||
Flags: strings.Join(c.Flags(), " "),
|
||||
Mode: string(ClientUser),
|
||||
Name: c.Name(),
|
||||
About: c.Edition(),
|
||||
Edition: c.Hub().Status,
|
||||
BaseUri: c.BaseUri(""),
|
||||
StaticUri: c.StaticUri(),
|
||||
CssUri: a.AppCssUri(),
|
||||
JsUri: a.AppJsUri(),
|
||||
ApiUri: c.ApiUri(),
|
||||
ContentUri: c.ContentUri(),
|
||||
SiteUrl: c.SiteUrl(),
|
||||
SiteDomain: c.SiteDomain(),
|
||||
SiteAuthor: c.SiteAuthor(),
|
||||
SiteTitle: c.SiteTitle(),
|
||||
SiteCaption: c.SiteCaption(),
|
||||
SiteDescription: c.SiteDescription(),
|
||||
SitePreview: c.SitePreview(),
|
||||
LegalInfo: c.LegalInfo(),
|
||||
LegalUrl: c.LegalUrl(),
|
||||
AppName: c.AppName(),
|
||||
AppMode: c.AppMode(),
|
||||
AppIcon: c.AppIcon(),
|
||||
WallpaperUri: c.WallpaperUri(),
|
||||
Version: c.Version(),
|
||||
Copyright: c.Copyright(),
|
||||
Debug: c.Debug(),
|
||||
Trace: c.Trace(),
|
||||
Test: c.Test(),
|
||||
Demo: c.Demo(),
|
||||
Sponsor: c.Sponsor(),
|
||||
ReadOnly: c.ReadOnly(),
|
||||
UploadNSFW: c.UploadNSFW(),
|
||||
Public: c.Public(),
|
||||
AuthMode: c.AuthMode(),
|
||||
LoginUri: c.LoginUri(),
|
||||
RegisterUri: c.RegisterUri(),
|
||||
PasswordLength: c.PasswordLength(),
|
||||
PasswordResetUri: c.PasswordResetUri(),
|
||||
Experimental: c.Experimental(),
|
||||
Albums: entity.Albums{},
|
||||
Cameras: entity.Cameras{},
|
||||
Lenses: entity.Lenses{},
|
||||
Countries: entity.Countries{},
|
||||
People: entity.People{},
|
||||
Colors: colors.All.List(),
|
||||
Thumbs: Thumbs,
|
||||
MapKey: c.Hub().MapKey(),
|
||||
DownloadToken: c.DownloadToken(),
|
||||
PreviewToken: c.PreviewToken(),
|
||||
ManifestUri: c.ClientManifestUri(),
|
||||
Clip: txt.ClipDefault,
|
||||
Server: env.Info(),
|
||||
Ext: ClientExt(c, ClientUser),
|
||||
}
|
||||
|
||||
hidePrivate := c.Settings().Features.Private
|
||||
|
|
|
@ -17,6 +17,10 @@ func TestConfig_ClientConfig(t *testing.T) {
|
|||
result := c.ClientPublic()
|
||||
assert.IsType(t, ClientConfig{}, result)
|
||||
assert.Equal(t, AuthModePublic, result.AuthMode)
|
||||
assert.Equal(t, "", result.LoginUri)
|
||||
assert.Equal(t, "", result.RegisterUri)
|
||||
assert.Equal(t, 0, result.PasswordLength)
|
||||
assert.Equal(t, "", result.PasswordResetUri)
|
||||
assert.Equal(t, true, result.Public)
|
||||
})
|
||||
t.Run("TestErrorConfig", func(t *testing.T) {
|
||||
|
|
|
@ -166,6 +166,9 @@ func (c *Config) Propagate() {
|
|||
places.UserAgent = c.UserAgent()
|
||||
entity.GeoApi = c.GeoApi()
|
||||
|
||||
// Set minimum password length.
|
||||
entity.PasswordLength = c.PasswordLength()
|
||||
|
||||
// Set API preview and download default tokens.
|
||||
entity.PreviewToken.Set(c.PreviewToken(), entity.TokenConfig)
|
||||
entity.DownloadToken.Set(c.DownloadToken(), entity.TokenConfig)
|
||||
|
@ -673,7 +676,7 @@ func (c *Config) AutoImport() time.Duration {
|
|||
return time.Duration(c.options.AutoImport) * time.Second
|
||||
}
|
||||
|
||||
// GeoApi returns the preferred geocoding api (none or places).
|
||||
// GeoApi returns the preferred geocoding api (places, or none).
|
||||
func (c *Config) GeoApi() string {
|
||||
if c.options.DisablePlaces {
|
||||
return ""
|
||||
|
|
|
@ -3,10 +3,9 @@ package config
|
|||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
@ -85,6 +84,11 @@ func (c *Config) SetAuthMode(mode string) {
|
|||
}
|
||||
}
|
||||
|
||||
// Auth checks if authentication is required.
|
||||
func (c *Config) Auth() bool {
|
||||
return !c.Public()
|
||||
}
|
||||
|
||||
// AuthMode returns the authentication mode.
|
||||
func (c *Config) AuthMode() string {
|
||||
if c.options.Public || c.options.Demo {
|
||||
|
@ -99,9 +103,46 @@ func (c *Config) AuthMode() string {
|
|||
}
|
||||
}
|
||||
|
||||
// Auth checks if authentication is required.
|
||||
func (c *Config) Auth() bool {
|
||||
return !c.Public()
|
||||
// LoginUri returns the user login URI.
|
||||
func (c *Config) LoginUri() string {
|
||||
if c.Public() {
|
||||
return ""
|
||||
}
|
||||
|
||||
if c.options.LoginUri == "" {
|
||||
return c.BaseUri("/library/login")
|
||||
}
|
||||
|
||||
return c.options.LoginUri
|
||||
}
|
||||
|
||||
// RegisterUri returns the user registration URI.
|
||||
func (c *Config) RegisterUri() string {
|
||||
if c.Public() {
|
||||
return ""
|
||||
}
|
||||
|
||||
return c.options.RegisterUri
|
||||
}
|
||||
|
||||
// PasswordLength returns the minimum password length in characters.
|
||||
func (c *Config) PasswordLength() int {
|
||||
if c.Public() {
|
||||
return 0
|
||||
} else if c.options.PasswordLength < 1 {
|
||||
return 4
|
||||
}
|
||||
|
||||
return c.options.PasswordLength
|
||||
}
|
||||
|
||||
// PasswordResetUri returns the password reset URI.
|
||||
func (c *Config) PasswordResetUri() string {
|
||||
if c.Public() {
|
||||
return ""
|
||||
}
|
||||
|
||||
return c.options.PasswordResetUri
|
||||
}
|
||||
|
||||
// CheckPassword compares given password p with the admin password
|
||||
|
|
|
@ -6,6 +6,18 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAuth(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
c.options.Public = true
|
||||
c.options.Demo = false
|
||||
assert.False(t, c.Auth())
|
||||
c.options.Public = false
|
||||
c.options.Demo = false
|
||||
assert.True(t, c.Auth())
|
||||
c.options.Demo = true
|
||||
assert.False(t, c.Auth())
|
||||
}
|
||||
|
||||
func TestAuthMode(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
c.options.Public = true
|
||||
|
@ -34,16 +46,24 @@ func TestAuthMode(t *testing.T) {
|
|||
c.options.Debug = false
|
||||
}
|
||||
|
||||
func TestAuth(t *testing.T) {
|
||||
func TestLoginUri(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
c.options.Public = true
|
||||
c.options.Demo = false
|
||||
assert.False(t, c.Auth())
|
||||
c.options.Public = false
|
||||
c.options.Demo = false
|
||||
assert.True(t, c.Auth())
|
||||
c.options.Demo = true
|
||||
assert.False(t, c.Auth())
|
||||
assert.Equal(t, "/library/login", c.LoginUri())
|
||||
}
|
||||
|
||||
func TestRegisterUri(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
assert.Equal(t, "", c.RegisterUri())
|
||||
}
|
||||
|
||||
func TestPasswordLength(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
assert.Equal(t, 4, c.PasswordLength())
|
||||
}
|
||||
|
||||
func TestPasswordResetUri(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
assert.Equal(t, "", c.PasswordResetUri())
|
||||
}
|
||||
|
||||
func TestSessMaxAge(t *testing.T) {
|
||||
|
|
|
@ -77,7 +77,7 @@ func (c *Config) HttpMode() string {
|
|||
return c.options.HttpMode
|
||||
}
|
||||
|
||||
// HttpCompression returns the http compression method (none or gzip).
|
||||
// HttpCompression returns the http compression method (gzip, or none).
|
||||
func (c *Config) HttpCompression() string {
|
||||
return strings.ToLower(strings.TrimSpace(c.options.HttpCompression))
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/klauspost/cpuid/v2"
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/face"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/server/header"
|
||||
|
@ -27,13 +30,13 @@ var Flags = CliFlags{
|
|||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "admin-user, login",
|
||||
Usage: "admin login `USERNAME`",
|
||||
Usage: "superadmin `USERNAME`",
|
||||
EnvVar: "PHOTOPRISM_ADMIN_USER",
|
||||
Value: "admin",
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "admin-password, pw",
|
||||
Usage: "initial admin `PASSWORD`, must have at least 8 characters",
|
||||
Usage: fmt.Sprintf("initial superadmin `PASSWORD` (minimum %d characters)", entity.PasswordLength),
|
||||
EnvVar: "PHOTOPRISM_ADMIN_PASSWORD",
|
||||
}}, {
|
||||
Flag: cli.Int64Flag{
|
||||
|
@ -126,10 +129,7 @@ var Flags = CliFlags{
|
|||
Value: DefaultResolutionLimit,
|
||||
Usage: "maximum resolution of media files in `MEGAPIXELS` (1-900; -1 to disable)",
|
||||
EnvVar: "PHOTOPRISM_RESOLUTION_LIMIT",
|
||||
},
|
||||
Tags: []string{EnvSponsor},
|
||||
},
|
||||
{
|
||||
}, Tags: []string{EnvSponsor}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "storage-path, s",
|
||||
Usage: "writable storage `PATH` for sidecar, cache, and database files",
|
||||
|
@ -427,12 +427,12 @@ var Flags = CliFlags{
|
|||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "http-mode, mode",
|
||||
Usage: "Web server `MODE` (debug, release, or test)",
|
||||
Usage: "Web server `MODE` (debug, release, test)",
|
||||
EnvVar: "PHOTOPRISM_HTTP_MODE",
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "http-compression, z",
|
||||
Usage: "Web server compression `METHOD` (none or gzip)",
|
||||
Usage: "Web server compression `METHOD` (gzip, none)",
|
||||
EnvVar: "PHOTOPRISM_HTTP_COMPRESSION",
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
|
|
|
@ -23,6 +23,10 @@ type Options struct {
|
|||
Copyright string `json:"-"`
|
||||
PartnerID string `yaml:"-" json:"-" flag:"partner-id"`
|
||||
AuthMode string `yaml:"AuthMode" json:"-" flag:"auth-mode"`
|
||||
LoginUri string `yaml:"LoginUri" json:"-" flag:"login-uri"`
|
||||
RegisterUri string `yaml:"RegisterUri" json:"-" flag:"register-uri"`
|
||||
PasswordLength int `yaml:"PasswordLength" json:"-" flag:"password-length"`
|
||||
PasswordResetUri string `yaml:"PasswordResetUri" json:"-" flag:"password-reset-uri"`
|
||||
Public bool `yaml:"Public" json:"-" flag:"public"`
|
||||
AdminUser string `yaml:"AdminUser" json:"-" flag:"admin-user"`
|
||||
AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"`
|
||||
|
|
|
@ -27,6 +27,10 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||
{"public", fmt.Sprintf("%t", c.Public())},
|
||||
{"session-maxage", fmt.Sprintf("%d", c.SessionMaxAge())},
|
||||
{"session-timeout", fmt.Sprintf("%d", c.SessionTimeout())},
|
||||
{"login-uri", c.LoginUri()},
|
||||
{"register-uri", c.RegisterUri()},
|
||||
{"password-length", fmt.Sprintf("%d", c.PasswordLength())},
|
||||
{"password-reset-uri", c.PasswordResetUri()},
|
||||
|
||||
// Logging.
|
||||
{"log-level", c.LogLevel().String()},
|
||||
|
|
|
@ -257,6 +257,26 @@ func (m *Session) SetUser(u *User) *Session {
|
|||
return m
|
||||
}
|
||||
|
||||
// Login returns the login name and provider.
|
||||
func (m *Session) Login() string {
|
||||
if m.AuthProvider == "" {
|
||||
return m.UserName
|
||||
} else {
|
||||
return fmt.Sprintf("%s@%s", m.UserName, m.AuthProvider)
|
||||
}
|
||||
}
|
||||
|
||||
// SetProvider updates the session's authentication provider.
|
||||
func (m *Session) SetProvider(provider string) *Session {
|
||||
if provider == "" {
|
||||
return m
|
||||
}
|
||||
|
||||
m.AuthProvider = provider
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// ChangePassword changes the password of the current user.
|
||||
func (m *Session) ChangePassword(newPw string) (err error) {
|
||||
u := m.User()
|
||||
|
|
|
@ -4,8 +4,6 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
|
@ -13,61 +11,90 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/server/limiter"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// Auth checks if the credentials are valid and returns the user and authentication provider.
|
||||
var Auth = func(f form.Login, m *Session, c *gin.Context) (user *User, provider string, err error) {
|
||||
name := f.Name()
|
||||
|
||||
user = FindUserByName(name)
|
||||
err = AuthPassword(user, f, m)
|
||||
|
||||
if err != nil {
|
||||
return user, ProviderNone, err
|
||||
}
|
||||
|
||||
return user, ProviderPassword, err
|
||||
}
|
||||
|
||||
// AuthPassword checks if the username and password are valid and returns the user.
|
||||
func AuthPassword(user *User, f form.Login, m *Session) (err error) {
|
||||
name := f.Name()
|
||||
|
||||
// User found?
|
||||
if user == nil {
|
||||
message := "account not found"
|
||||
limiter.Login.Reserve(m.IP())
|
||||
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
return i18n.Error(i18n.ErrInvalidCredentials)
|
||||
}
|
||||
|
||||
// Login allowed?
|
||||
if !user.CanLogIn() {
|
||||
message := "account disabled"
|
||||
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
return i18n.Error(i18n.ErrInvalidCredentials)
|
||||
}
|
||||
|
||||
// Password valid?
|
||||
if user.WrongPassword(f.Password) {
|
||||
message := "incorrect password"
|
||||
limiter.Login.Reserve(m.IP())
|
||||
event.AuditErr([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
return i18n.Error(i18n.ErrInvalidCredentials)
|
||||
} else {
|
||||
event.AuditInfo([]string{m.IP(), "session %s", "login as %s", "succeeded"}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginInfo(m.IP(), "api", name, m.UserAgent)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// LogIn performs authentication checks against the specified login form.
|
||||
func (m *Session) LogIn(f form.Login, c *gin.Context) (err error) {
|
||||
if c != nil {
|
||||
m.SetContext(c)
|
||||
}
|
||||
|
||||
// Username and password provided?
|
||||
var user *User
|
||||
var provider string
|
||||
|
||||
// Login credentials provided?
|
||||
if f.HasCredentials() {
|
||||
if m.IsRegistered() {
|
||||
m.RegenerateID()
|
||||
}
|
||||
|
||||
name := f.Name()
|
||||
user := FindUserByName(name)
|
||||
user, provider, err = Auth(f, m, c)
|
||||
|
||||
// User found?
|
||||
if user == nil {
|
||||
message := "account not found"
|
||||
limiter.Login.Reserve(m.IP())
|
||||
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
return i18n.Error(i18n.ErrInvalidCredentials)
|
||||
}
|
||||
|
||||
// Login allowed?
|
||||
if !user.CanLogIn() {
|
||||
message := "account disabled"
|
||||
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
return i18n.Error(i18n.ErrInvalidCredentials)
|
||||
}
|
||||
|
||||
// Password valid?
|
||||
if user.WrongPassword(f.Password) {
|
||||
message := "incorrect password"
|
||||
limiter.Login.Reserve(m.IP())
|
||||
event.AuditErr([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
return i18n.Error(i18n.ErrInvalidCredentials)
|
||||
} else {
|
||||
event.AuditInfo([]string{m.IP(), "session %s", "login as %s", "succeeded"}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginInfo(m.IP(), "api", name, m.UserAgent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.SetUser(user)
|
||||
m.SetProvider(provider)
|
||||
}
|
||||
|
||||
// Share token provided?
|
||||
if f.HasToken() {
|
||||
user := m.User()
|
||||
user = m.User()
|
||||
|
||||
// Redeem token.
|
||||
if user.IsRegistered() {
|
||||
|
|
|
@ -8,9 +8,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ulule/deepcopier"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/ulule/deepcopier"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
|
@ -27,11 +26,11 @@ const (
|
|||
OwnerUnknown = ""
|
||||
)
|
||||
|
||||
// LenNameMin specifies the minimum length of the username in characters.
|
||||
var LenNameMin = 3
|
||||
// UsernameLength specifies the minimum length of the username in characters.
|
||||
var UsernameLength = 1
|
||||
|
||||
// LenPasswordMin specifies the minimum length of the password in characters.
|
||||
var LenPasswordMin = 4
|
||||
// PasswordLength specifies the minimum length of the password in characters.
|
||||
var PasswordLength = 4
|
||||
|
||||
// Users represents a list of users.
|
||||
type Users []User
|
||||
|
@ -110,6 +109,8 @@ func FindUser(find User) *User {
|
|||
stmt = stmt.Where("user_name = ?", find.UserName)
|
||||
} else if find.UserEmail != "" {
|
||||
stmt = stmt.Where("user_email = ?", find.UserEmail)
|
||||
} else if find.AuthProvider != "" && find.AuthID != "" {
|
||||
stmt = stmt.Where("auth_provider = ? AND auth_id = ?", find.AuthProvider, find.AuthID)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
@ -512,7 +513,16 @@ func (m *User) IsAdmin() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
return m.IsRegistered() && (m.SuperAdmin || m.AclRole() == acl.RoleAdmin)
|
||||
return m.IsSuperAdmin() || m.IsRegistered() && m.AclRole() == acl.RoleAdmin
|
||||
}
|
||||
|
||||
// IsSuperAdmin checks if the user is a super admin.
|
||||
func (m *User) IsSuperAdmin() bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return m.SuperAdmin
|
||||
}
|
||||
|
||||
// IsVisitor checks if the user is a sharing link visitor.
|
||||
|
@ -559,8 +569,8 @@ func (m *User) SetPassword(password string) error {
|
|||
return fmt.Errorf("only registered users can change their password")
|
||||
}
|
||||
|
||||
if len(password) < LenPasswordMin {
|
||||
return fmt.Errorf("password must have at least %d characters", LenPasswordMin)
|
||||
if len(password) < PasswordLength {
|
||||
return fmt.Errorf("password must have at least %d characters", PasswordLength)
|
||||
}
|
||||
|
||||
pw := NewPassword(m.UserUID, password)
|
||||
|
@ -614,8 +624,8 @@ func (m *User) Validate() (err error) {
|
|||
}
|
||||
|
||||
// Name too short?
|
||||
if len(m.Name()) < LenNameMin {
|
||||
return fmt.Errorf("username must have at least %d characters", LenNameMin)
|
||||
if len(m.Name()) < UsernameLength {
|
||||
return fmt.Errorf("username must have at least %d characters", UsernameLength)
|
||||
}
|
||||
|
||||
// Validate user role.
|
||||
|
@ -867,3 +877,12 @@ func (m *User) SetAvatar(thumb, thumbSrc string) error {
|
|||
|
||||
return m.Updates(Values{"Thumb": m.Thumb, "ThumbSrc": m.ThumbSrc})
|
||||
}
|
||||
|
||||
// Login returns the login name and provider.
|
||||
func (m *User) Login() string {
|
||||
if m.AuthProvider == "" {
|
||||
return m.UserName
|
||||
} else {
|
||||
return fmt.Sprintf("%s@%s", m.UserName, m.AuthProvider)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,8 +13,8 @@ import (
|
|||
func AddUser(frm form.User) error {
|
||||
user := NewUser().SetFormValues(frm)
|
||||
|
||||
if len(frm.Password) < LenPasswordMin {
|
||||
return fmt.Errorf("password must have at least %d characters", LenPasswordMin)
|
||||
if len(frm.Password) < PasswordLength {
|
||||
return fmt.Errorf("password must have at least %d characters", PasswordLength)
|
||||
}
|
||||
|
||||
if err := user.Validate(); err != nil {
|
||||
|
|
|
@ -333,6 +333,7 @@ func TestFindUserByUID(t *testing.T) {
|
|||
assert.Equal(t, "alice@example.com", m.UserEmail)
|
||||
assert.True(t, m.SuperAdmin)
|
||||
assert.True(t, m.IsAdmin())
|
||||
assert.True(t, m.IsSuperAdmin())
|
||||
assert.False(t, m.IsVisitor())
|
||||
assert.True(t, m.CanLogin)
|
||||
assert.NotEmpty(t, m.CreatedAt)
|
||||
|
@ -352,6 +353,7 @@ func TestFindUserByUID(t *testing.T) {
|
|||
assert.Equal(t, "bob@example.com", m.UserEmail)
|
||||
assert.False(t, m.SuperAdmin)
|
||||
assert.True(t, m.IsAdmin())
|
||||
assert.False(t, m.IsSuperAdmin())
|
||||
assert.False(t, m.IsVisitor())
|
||||
assert.True(t, m.CanLogin)
|
||||
assert.NotEmpty(t, m.CreatedAt)
|
||||
|
@ -537,15 +539,6 @@ func TestUser_Validate(t *testing.T) {
|
|||
|
||||
assert.Error(t, u.Validate())
|
||||
})
|
||||
t.Run("NameTooShort", func(t *testing.T) {
|
||||
u := &User{
|
||||
UserName: "va",
|
||||
DisplayName: "Validate",
|
||||
UserEmail: "validate@example.com",
|
||||
UserRole: acl.RoleAdmin.String(),
|
||||
}
|
||||
assert.Error(t, u.Validate())
|
||||
})
|
||||
t.Run("NameNotUnique", func(t *testing.T) {
|
||||
FirstOrCreateUser(&User{
|
||||
UserName: "notunique1",
|
||||
|
|
|
@ -52,6 +52,12 @@ const (
|
|||
IsUnstacked int8 = -1
|
||||
)
|
||||
|
||||
// Authentication providers.
|
||||
const (
|
||||
ProviderNone = ""
|
||||
ProviderPassword = "password"
|
||||
)
|
||||
|
||||
// Sort options.
|
||||
const (
|
||||
SortOrderDefault = ""
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
"github.com/jinzhu/gorm"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/mutex"
|
||||
|
|
|
@ -4,18 +4,17 @@ import (
|
|||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
)
|
||||
|
||||
// registerStaticRoutes configures serving static assets and templates.
|
||||
func registerStaticRoutes(router *gin.Engine, conf *config.Config) {
|
||||
// Redirects to the PWA for now, can be replaced by a template later.
|
||||
router.GET(conf.BaseUri("/"), func(c *gin.Context) {
|
||||
c.Redirect(http.StatusTemporaryRedirect, conf.BaseUri("/library/login"))
|
||||
c.Redirect(http.StatusTemporaryRedirect, conf.LoginUri())
|
||||
})
|
||||
|
||||
// Shows "Page Not found" error if no other handler is registered.
|
||||
|
|
Loading…
Add table
Reference in a new issue