Passwords: Enforce 72-character limit and improve bcrypt support #1987

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2023-04-13 17:21:18 +02:00
parent 3575ccaec7
commit 163398b76c
20 changed files with 231 additions and 119 deletions

View file

@ -20,7 +20,7 @@ services:
environment:
PHOTOPRISM_INIT: "https"
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial superadmin password (minimum 8 characters)
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial superadmin password (8-72 characters)
PHOTOPRISM_AUTH_MODE: "public" # authentication mode (public, password)
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"

View file

@ -25,7 +25,7 @@ services:
PHOTOPRISM_UID: ${UID:-1000} # user id, should match your host user id
PHOTOPRISM_GID: ${GID:-1000} # group id
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial superadmin password (minimum 8 characters)
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial superadmin password (8-72 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"

View file

@ -26,7 +26,7 @@ services:
PHOTOPRISM_UID: ${UID:-1000} # user id, should match your host user id
PHOTOPRISM_GID: ${GID:-1000} # group id
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial superadmin password (minimum 8 characters)
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial superadmin password (8-72 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"

View file

@ -28,7 +28,7 @@ services:
environment:
PHOTOPRISM_INIT: "https"
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial superadmin password (minimum 8 characters)
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial superadmin password (8-72 characters)
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"

View file

@ -42,7 +42,7 @@ services:
PHOTOPRISM_GID: ${GID:-1000} # group id
## Access Management
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial superadmin password (minimum 8 characters)
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial superadmin password (8-72 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"

View file

@ -1,59 +1,68 @@
<template>
<v-dialog :value="show" lazy persistent max-width="500" class="modal-dialog p-account-password-dialog" @keydown.esc="cancel">
<v-dialog :value="show" lazy persistent max-width="500" class="modal-dialog p-account-password-dialog"
@keydown.esc="cancel">
<v-form ref="form" dense class="form-password" accept-charset="UTF-8">
<v-card raised elevation="24">
<v-card-title primary-title class="pa-2">
<v-layout row wrap class="pa-2">
<v-flex xs9 class="text-xs-left">
<h3 class="headline pa-0"><translate>Change Password</translate></h3>
</v-flex>
<v-flex xs3 class="text-xs-right">
<v-icon size="28" color="primary">lock</v-icon>
</v-flex>
</v-layout>
</v-card-title>
<v-card-text class="py-0 px-2">
<v-layout wrap align-top>
<v-flex v-if="oldRequired" xs12 class="px-2 pb-2 caption">
<translate>Please note that changing your password will log you out on other devices and browsers.</translate>
</v-flex>
<v-flex v-if="oldRequired" xs12 class="px-2 py-1">
<v-text-field
<v-card raised elevation="24">
<v-card-title primary-title class="pa-2">
<v-layout row wrap class="pa-2">
<v-flex xs9 class="text-xs-left">
<h3 class="headline pa-0">
<translate>Change Password</translate>
</h3>
</v-flex>
<v-flex xs3 class="text-xs-right">
<v-icon size="28" color="primary">lock</v-icon>
</v-flex>
</v-layout>
</v-card-title>
<v-card-text class="py-0 px-2">
<v-layout wrap align-top>
<v-flex v-if="oldRequired" xs12 class="px-2 pb-2 caption">
<translate>Please note that changing your password will log you out on other devices and browsers.
</translate>
</v-flex>
<v-flex v-if="oldRequired" xs12 class="px-2 py-1">
<v-text-field
v-model="oldPassword"
hide-details required box flat
type="password"
:disabled="busy"
:maxlength="maxLength"
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
:label="$gettext('Current Password')"
class="input-current-password"
color="secondary-dark"
></v-text-field>
</v-flex>
></v-text-field>
</v-flex>
<v-flex xs12 class="px-2 py-1">
<v-text-field
<v-flex xs12 class="px-2 py-1">
<v-text-field
v-model="newPassword"
required counter persistent-hint box flat
type="password"
:disabled="busy"
:minlength="minLength"
:maxlength="maxLength"
browser-autocomplete="new-password"
autocorrect="off"
autocapitalize="none"
:label="$gettext('New Password')"
class="input-new-password"
color="secondary-dark"
:hint="$gettextInterpolate($gettext('Must have at least %{n} characters.'), {n: passwordLength})"
></v-text-field>
</v-flex>
:hint="$gettextInterpolate($gettext('Must have at least %{n} characters.'), {n: minLength})"
></v-text-field>
</v-flex>
<v-flex xs12 class="px-2 py-1">
<v-text-field
<v-flex xs12 class="px-2 py-1">
<v-text-field
v-model="confirmPassword"
required counter persistent-hint box flat
type="password"
:disabled="busy"
:minlength="minLength"
:maxlength="maxLength"
browser-autocomplete="new-password"
autocorrect="off"
autocapitalize="none"
@ -62,28 +71,28 @@
color="secondary-dark"
:hint="$gettext('Please confirm your new password.')"
@keyup.enter.native="confirm"
></v-text-field>
</v-flex>
</v-layout>
</v-card-text>
<v-card-actions class="pt-1 pb-2 px-2">
<v-layout row wrap class="pa-2">
<v-flex xs12 text-xs-right>
<v-btn depressed color="secondary-light"
class="action-cancel ml-0"
@click.stop="cancel">
<translate>Cancel</translate>
</v-btn>
<v-btn depressed color="primary-button"
class="action-confirm white--text compact mr-0"
:disabled="disabled()"
@click.stop="confirm">
<translate>Save</translate>
</v-btn>
</v-flex>
</v-layout>
</v-card-actions>
</v-card>
></v-text-field>
</v-flex>
</v-layout>
</v-card-text>
<v-card-actions class="pt-1 pb-2 px-2">
<v-layout row wrap class="pa-2">
<v-flex xs12 text-xs-right>
<v-btn depressed color="secondary-light"
class="action-cancel ml-0"
@click.stop="cancel">
<translate>Cancel</translate>
</v-btn>
<v-btn depressed color="primary-button"
class="action-confirm white--text compact mr-0"
:disabled="disabled()"
@click.stop="confirm">
<translate>Save</translate>
</v-btn>
</v-flex>
</v-layout>
</v-card-actions>
</v-card>
</v-form>
</v-dialog>
</template>
@ -107,7 +116,8 @@ export default {
oldPassword: "",
newPassword: "",
confirmPassword: "",
passwordLength: this.$config.get("passwordLength"),
minLength: this.$config.get("passwordLength"),
maxLength: 72,
rtl: this.$rtl,
};
},
@ -123,13 +133,17 @@ export default {
},
},
created() {
if(this.isPublic && !this.isDemo) {
if (this.isPublic && !this.isDemo) {
this.$emit('cancel');
}
},
methods: {
disabled() {
return (this.isDemo || this.busy || this.oldPassword === "" && this.oldRequired || this.newPassword.length < this.passwordLength || (this.newPassword !== this.confirmPassword));
return (this.isDemo || this.busy
|| this.oldPassword === "" && this.oldRequired
|| this.newPassword.length < this.minLength
|| this.newPassword.length > this.maxLength
|| (this.newPassword !== this.confirmPassword));
},
confirm() {
this.busy = true;

View file

@ -22,7 +22,13 @@ var PasswdCommand = cli.Command{
Name: "passwd",
Usage: "Changes the password of the user specified as argument",
ArgsUsage: "[username]",
Action: passwdAction,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "show, s",
Usage: "show bcrypt password hash",
},
},
Action: passwdAction,
}
// passwdAction changes the password of the user specified as command argument.
@ -79,7 +85,12 @@ func passwdAction(ctx *cli.Context) error {
return err
}
log.Infof("changed password for %s\n", clean.Log(m.Username()))
// Show bcrypt password hash?
if pw := entity.FindPassword(m.UserUID); ctx.Bool("show") && pw != nil {
log.Infof("password for %s successfully changed to %s\n", clean.Log(m.Username()), pw.Hash)
} else {
log.Infof("password for %s successfully changed\n", clean.Log(m.Username()))
}
return nil
}

View file

@ -12,6 +12,7 @@ import (
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/server/header"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/txt"
)
// Flags configures the global command-line interface (CLI) parameters.
@ -37,7 +38,7 @@ var Flags = CliFlags{
}}, {
Flag: cli.StringFlag{
Name: "admin-password, pw",
Usage: fmt.Sprintf("initial superadmin `PASSWORD` (minimum %d characters)", entity.PasswordLength),
Usage: fmt.Sprintf("initial superadmin `PASSWORD` (%d-%d characters)", entity.PasswordLength, txt.ClipPassword),
EnvVar: EnvVar("ADMIN_PASSWORD"),
}}, {
Flag: cli.Int64Flag{

View file

@ -1,9 +1,17 @@
package entity
import (
"fmt"
"time"
"golang.org/x/crypto/bcrypt"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
)
var (
PasswordCost = 14
)
// Password represents a password hash.
@ -20,15 +28,15 @@ func (Password) TableName() string {
}
// NewPassword creates a new password instance.
func NewPassword(uid, password string) Password {
func NewPassword(uid, pw string) Password {
if uid == "" {
panic("auth: cannot set password without uid")
}
m := Password{UID: uid}
if password != "" {
if err := m.SetPassword(password); err != nil {
if pw != "" {
if err := m.SetPassword(pw); err != nil {
log.Errorf("auth: failed setting password for %s", uid)
}
}
@ -37,8 +45,23 @@ func NewPassword(uid, password string) Password {
}
// SetPassword sets a new password stored as hash.
func (m *Password) SetPassword(password string) error {
if bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14); err != nil {
func (m *Password) SetPassword(s string) error {
s = clean.Password(s)
if l := len(s); l > txt.ClipPassword {
return fmt.Errorf("password is too long")
} else if l < 1 {
return fmt.Errorf("password is too short")
}
// Check if string already is a bcrypt hash.
if cost, err := bcrypt.Cost([]byte(s)); err == nil && cost >= bcrypt.MinCost {
m.Hash = s
return nil
}
// Generate hash from plain text string.
if bytes, err := bcrypt.GenerateFromPassword([]byte(s), PasswordCost); err != nil {
return err
} else {
m.Hash = string(bytes)
@ -46,19 +69,26 @@ func (m *Password) SetPassword(password string) error {
}
}
// Is checks if the password is correct.
func (m *Password) Is(s string) bool {
// IsValid checks if the password is correct.
func (m *Password) IsValid(s string) bool {
return !m.IsWrong(s)
}
// IsWrong checks if the specified password is incorrect.
func (m *Password) IsWrong(s string) bool {
if m.Hash == "" && s == "" {
return false
if m.IsEmpty() {
// No password set.
return true
} else if s = clean.Password(s); s == "" {
// No password provided.
return true
} else if err := bcrypt.CompareHashAndPassword([]byte(m.Hash), []byte(s)); err != nil {
// Wrong password.
return true
}
err := bcrypt.CompareHashAndPassword([]byte(m.Hash), []byte(s))
return err != nil
// Ok.
return false
}
// Create inserts a new row to the database.
@ -82,12 +112,21 @@ func FindPassword(uid string) *Password {
return nil
}
// Cost returns the hashing cost of the currently set password.
func (m *Password) Cost() (int, error) {
if m.IsEmpty() {
return 0, fmt.Errorf("password is empty")
}
return bcrypt.Cost([]byte(m.Hash))
}
// IsEmpty returns true if the password is not set.
func (m *Password) IsEmpty() bool {
return m.Hash == ""
}
// String returns the password hash.
func (m *Password) String() string {
return m.Hash
}
// Unknown returns true if the password is an empty string.
func (m *Password) Unknown() bool {
return m.Hash == ""
}

View file

@ -8,57 +8,85 @@ import (
func TestNewPassword(t *testing.T) {
t.Run("success", func(t *testing.T) {
p := NewPassword("abc567", "passwd")
p := NewPassword("urrwaxd19ldtz68x", "passwd")
assert.Len(t, p.Hash, 60)
})
t.Run("empty password", func(t *testing.T) {
p := NewPassword("abc567", "")
p := NewPassword("urrwaxd19ldtz68x", "")
assert.Equal(t, "", p.Hash)
})
}
func TestPassword_SetPassword(t *testing.T) {
t.Run("success", func(t *testing.T) {
p := NewPassword("abc567", "passwd")
t.Run("Text", func(t *testing.T) {
p := NewPassword("urrwaxd19ldtz68x", "passwd")
assert.Len(t, p.Hash, 60)
assert.True(t, p.IsValid("passwd"))
assert.False(t, p.IsValid("other"))
if err := p.SetPassword("abcd"); err != nil {
t.Fatal(err)
}
assert.Len(t, p.Hash, 60)
assert.True(t, p.IsValid("abcd"))
assert.False(t, p.IsValid("other"))
})
t.Run("Hash", func(t *testing.T) {
p := NewPassword("urrwaxd19ldtz68x", "$2a$14$qCcNjxupSJV1gjhgdYxz8e9l0e0fTZosX0s0qhMK54IkI9YOyWLt2")
assert.Len(t, p.Hash, 60)
assert.True(t, p.IsValid("photoprism"))
assert.False(t, p.IsValid("$2a$14$qCcNjxupSJV1gjhgdYxz8e9l0e0fTZosX0s0qhMK54IkI9YOyWLt2"))
assert.False(t, p.IsValid("other"))
})
}
func TestPassword_Is(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
func TestPassword_IsValid(t *testing.T) {
t.Run("EmptyHash", func(t *testing.T) {
p := Password{Hash: ""}
assert.True(t, p.Is(""))
assert.True(t, p.IsEmpty())
assert.False(t, p.IsValid(""))
})
t.Run("Ok", func(t *testing.T) {
p := NewPassword("abc567", "")
assert.True(t, p.Is(""))
t.Run("EmptyPassword", func(t *testing.T) {
p := NewPassword("urrwaxd19ldtz68x", "")
assert.True(t, p.IsEmpty())
assert.False(t, p.IsValid(""))
})
t.Run("Wrong", func(t *testing.T) {
p := NewPassword("abc567", "passwd")
assert.False(t, p.Is("$2a$14$p3HKuLvrTuePG/pjXLJQseUnSeAVeVO2cy4b0.34KXsLPK8lkI92G"))
t.Run("ShortPassword", func(t *testing.T) {
p := NewPassword("urrwaxd19ldtz68x", "passwd")
assert.True(t, p.IsValid("passwd"))
assert.False(t, p.IsValid("$2a$14$p3HKuLvrTuePG/pjXLJQseUnSeAVeVO2cy4b0.34KXsLPK8lkI92G"))
})
t.Run("LongPassword", func(t *testing.T) {
p := NewPassword("urrwaxd19ldtz68x", "photoprism")
assert.True(t, p.IsValid("photoprism"))
assert.False(t, p.IsValid("$2a$14$p3HKuLvrTuePG/pjXLJQseUnSeAVeVO2cy4b0.34KXsLPK8lkI92G"))
})
}
func TestPassword_IsWrong(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
t.Run("EmptyHash", func(t *testing.T) {
p := Password{Hash: ""}
assert.False(t, p.IsWrong(""))
assert.True(t, p.IsEmpty())
assert.True(t, p.IsWrong(""))
})
t.Run("Ok", func(t *testing.T) {
p := NewPassword("abc567", "")
assert.False(t, p.IsWrong(""))
t.Run("EmptyPassword", func(t *testing.T) {
p := NewPassword("urrwaxd19ldtz68x", "")
assert.True(t, p.IsEmpty())
assert.True(t, p.IsWrong(""))
})
t.Run("Wrong", func(t *testing.T) {
p := NewPassword("abc567", "passwd")
t.Run("ShortPassword", func(t *testing.T) {
p := NewPassword("urrwaxd19ldtz68x", "passwd")
assert.True(t, p.IsWrong("$2a$14$p3HKuLvrTuePG/pjXLJQseUnSeAVeVO2cy4b0.34KXsLPK8lkI92G"))
assert.False(t, p.IsWrong("passwd"))
})
t.Run("LongPassword", func(t *testing.T) {
p := NewPassword("urrwaxd19ldtz68x", "photoprism")
assert.True(t, p.IsWrong("$2a$14$p3HKuLvrTuePG/pjXLJQseUnSeAVeVO2cy4b0.34KXsLPK8lkI92G"))
assert.False(t, p.IsWrong("photoprism"))
})
}
// TODO fails on mariadb
func TestPassword_Create(t *testing.T) {
t.Run("success", func(t *testing.T) {
p := Password{}
@ -72,26 +100,26 @@ func TestPassword_Create(t *testing.T) {
}
func TestFindPassword(t *testing.T) {
t.Run("not existing", func(t *testing.T) {
t.Run("NotFound", func(t *testing.T) {
r := FindPassword("xxx")
assert.Nil(t, r)
})
t.Run("existing", func(t *testing.T) {
p := NewPassword("abc567", "passwd")
t.Run("Exists", func(t *testing.T) {
p := NewPassword("urrwaxd19ldtz68x", "passwd")
if err := p.Save(); err != nil {
t.Fatal(err)
}
r := FindPassword("abc567")
r := FindPassword("urrwaxd19ldtz68x")
assert.NotEmpty(t, r)
})
t.Run("alice", func(t *testing.T) {
t.Run("Alice", func(t *testing.T) {
if p := FindPassword("uqxetse3cy5eo9z2"); p == nil {
t.Fatal("password not found")
} else {
assert.False(t, p.IsWrong("Alice123!"))
}
})
t.Run("bob", func(t *testing.T) {
t.Run("Bob", func(t *testing.T) {
if p := FindPassword("uqxc08w3d0ej2283"); p == nil {
t.Fatal("password not found")
} else {
@ -100,20 +128,39 @@ func TestFindPassword(t *testing.T) {
})
}
func TestPassword_Cost(t *testing.T) {
t.Run("Default", func(t *testing.T) {
p := NewPassword("urrwaxd19ldtz68x", "photoprism")
if cost, err := p.Cost(); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, PasswordCost, cost)
}
})
t.Run("14", func(t *testing.T) {
p := NewPassword("urrwaxd19ldtz68x", "$2a$14$qCcNjxupSJV1gjhgdYxz8e9l0e0fTZosX0s0qhMK54IkI9YOyWLt2")
if cost, err := p.Cost(); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 14, cost)
}
})
}
func TestPassword_String(t *testing.T) {
t.Run("return string", func(t *testing.T) {
p := NewPassword("abc567", "lkjhgtyu")
p := NewPassword("urrwaxd19ldtz68x", "lkjhgtyu")
assert.Len(t, p.String(), 60)
})
}
func TestPassword_Unknown(t *testing.T) {
func TestPassword_IsEmpty(t *testing.T) {
t.Run("false", func(t *testing.T) {
p := NewPassword("abc567", "lkjhgtyu")
assert.False(t, p.Unknown())
p := NewPassword("urrwaxd19ldtz68x", "lkjhgtyu")
assert.False(t, p.IsEmpty())
})
t.Run("true", func(t *testing.T) {
p := Password{}
assert.True(t, p.Unknown())
assert.True(t, p.IsEmpty())
})
}

View file

@ -12,6 +12,7 @@ const (
ClipIP = 48
ClipRealm = 64
ClipUserName = 64
ClipPassword = 72
ClipSlug = 80
ClipCategory = 100
ClipTokenName = 128
@ -27,7 +28,6 @@ const (
ClipShortText = 1024
ClipText = 2048
ClipLongText = 4096
ClipPassword = 4096
)
// Clip shortens a string to the given number of runes, and removes all leading and trailing white space.

View file

@ -52,7 +52,7 @@ services:
- "2342:2342" # HTTP port (host:container)
environment:
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (minimum 8 characters)
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (8-72 characters)
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/" # server URL in the format "http(s)://domain.name(:port)/(path)"
PHOTOPRISM_ORIGINALS_LIMIT: 5000 # file size limit for originals in MB (increase for high-res video)

View file

@ -47,7 +47,7 @@ services:
- "2342:2342" # HTTP port (host:container)
environment:
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (minimum 8 characters)
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (8-72 characters)
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/" # server URL in the format "http(s)://domain.name(:port)/(path)"
PHOTOPRISM_ORIGINALS_LIMIT: 5000 # file size limit for originals in MB (increase for high-res video)

View file

@ -44,7 +44,7 @@ services:
- "2342:2342" # HTTP port (host:container)
environment:
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (minimum 8 characters)
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (8-72 characters)
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/" # server URL in the format "http(s)://domain.name(:port)/(path)"
PHOTOPRISM_ORIGINALS_LIMIT: 5000 # file size limit for originals in MB (increase for high-res video)

View file

@ -40,7 +40,7 @@ services:
- "2342:2342" # HTTP port (host:container)
environment:
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (minimum 8 characters)
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (8-72 characters)
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/" # server URL in the format "http(s)://domain.name(:port)/(path)"
PHOTOPRISM_ORIGINALS_LIMIT: 5000 # file size limit for originals in MB (increase for high-res video)

View file

@ -48,7 +48,7 @@ services:
- "2342:2342" # HTTP port (host:container)
environment:
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (minimum 8 characters)
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (8-72 characters)
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/" # server URL in the format "http(s)://domain.name(:port)/(path)"
PHOTOPRISM_ORIGINALS_LIMIT: 5000 # file size limit for originals in MB (increase for high-res video)

View file

@ -42,7 +42,7 @@ services:
- "2342:2342" # HTTP port (host:container)
environment:
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (minimum 8 characters)
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (8-72 characters)
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/" # server URL in the format "http(s)://domain.name(:port)/(path)"
PHOTOPRISM_ORIGINALS_LIMIT: 5000 # file size limit for originals in MB (increase for high-res video)

View file

@ -40,7 +40,7 @@ services:
- "2342:2342" # HTTP port (host:container)
environment:
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (minimum 8 characters)
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (8-72 characters)
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/" # server URL in the format "http(s)://domain.name(:port)/(path)"
PHOTOPRISM_ORIGINALS_LIMIT: 5000 # file size limit for originals in MB (increase for high-res video)

View file

@ -46,7 +46,7 @@ services:
- "2342:2342" # HTTP port (host:container)
environment:
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (minimum 8 characters)
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (8-72 characters)
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/" # server URL in the format "http(s)://domain.name(:port)/(path)"
PHOTOPRISM_ORIGINALS_LIMIT: 5000 # file size limit for originals in MB (increase for high-res video)

View file

@ -48,7 +48,7 @@ services:
- "2342:2342" # HTTP port (host:container)
environment:
PHOTOPRISM_ADMIN_USER: "admin" # superadmin username
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (minimum 8 characters)
PHOTOPRISM_ADMIN_PASSWORD: "insecure" # initial superadmin password (8-72 characters)
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/" # server URL in the format "http(s)://domain.name(:port)/(path)"
PHOTOPRISM_ORIGINALS_LIMIT: 5000 # file size limit for originals in MB (increase for high-res video)