Auth: Extend account settings with user details and avatar upload #98
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
2cf420d04a
commit
837669f796
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-08-01 15:07+0000\n"
|
||||
"POT-Creation-Date: 2022-10-17 14:51+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -17,331 +17,359 @@ msgstr ""
|
|||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: messages.go:87
|
||||
#: messages.go:94
|
||||
msgid "Unexpected error, please try again"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:88
|
||||
#: messages.go:95
|
||||
msgid "Invalid request"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:89
|
||||
#: messages.go:96
|
||||
msgid "Changes could not be saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:90
|
||||
#: messages.go:97
|
||||
msgid "Could not be deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:91
|
||||
#: messages.go:98
|
||||
#, c-format
|
||||
msgid "%s already exists"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:92
|
||||
#: messages.go:99
|
||||
msgid "Not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:93
|
||||
#: messages.go:100
|
||||
msgid "File not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:94
|
||||
msgid "Originals folder is empty"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:95
|
||||
msgid "Selection not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:96
|
||||
msgid "Entity not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:97
|
||||
msgid "Account not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:98
|
||||
msgid "User not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:99
|
||||
msgid "Label not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:100
|
||||
msgid "Album not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:101
|
||||
msgid "Subject not found"
|
||||
msgid "File too large"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:102
|
||||
msgid "Person not found"
|
||||
msgid "Wrong file type"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:103
|
||||
msgid "Face not found"
|
||||
msgid "Originals folder is empty"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:104
|
||||
msgid "Not available in public mode"
|
||||
msgid "Selection not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:105
|
||||
msgid "not available in read-only mode"
|
||||
msgid "Entity not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:106
|
||||
msgid "Please log in and try again"
|
||||
msgid "Account not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:107
|
||||
msgid "Upload might be offensive"
|
||||
msgid "User not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:108
|
||||
msgid "No items selected"
|
||||
msgid "Label not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:109
|
||||
msgid "Failed creating file, please check permissions"
|
||||
msgid "Album not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:110
|
||||
msgid "Failed creating folder, please check permissions"
|
||||
msgid "Subject not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:111
|
||||
msgid "Could not connect, please try again"
|
||||
msgid "Person not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:112
|
||||
msgid "Invalid password, please try again"
|
||||
msgid "Face not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:113
|
||||
msgid "Feature disabled"
|
||||
msgid "Not available in public mode"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:114
|
||||
msgid "No labels selected"
|
||||
msgid "Not available in read-only mode"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:115
|
||||
msgid "No albums selected"
|
||||
msgid "Please log in to your account"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:116
|
||||
msgid "No files available for download"
|
||||
msgid "Permission denied"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:117
|
||||
msgid "Failed to create zip file"
|
||||
msgid "Upload might be offensive"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:118
|
||||
msgid "Invalid credentials"
|
||||
msgid "Upload failed"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:119
|
||||
msgid "Invalid link"
|
||||
msgid "No items selected"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:120
|
||||
msgid "Invalid name"
|
||||
msgid "RunFailed creating file, please check permissions"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:121
|
||||
msgid "Busy, please try again later"
|
||||
msgid "RunFailed creating folder, please check permissions"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:122
|
||||
#, c-format
|
||||
msgid "The wakeup interval is %s, but must be 1h or less"
|
||||
msgid "Could not connect, please try again"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:123
|
||||
msgid "Your account could not be connected"
|
||||
msgid "Invalid password, please try again"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:124
|
||||
msgid "Feature disabled"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:125
|
||||
msgid "No labels selected"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:126
|
||||
msgid "Changes successfully saved"
|
||||
msgid "No albums selected"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:127
|
||||
msgid "Album created"
|
||||
msgid "No files available for download"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:128
|
||||
msgid "Album saved"
|
||||
msgid "RunFailed to create zip file"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:129
|
||||
#, c-format
|
||||
msgid "Album %s deleted"
|
||||
msgid "Invalid credentials"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:130
|
||||
msgid "Album contents cloned"
|
||||
msgid "Invalid link"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:131
|
||||
msgid "File removed from stack"
|
||||
msgid "Invalid name"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:132
|
||||
msgid "File deleted"
|
||||
msgid "Busy, please try again later"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:133
|
||||
#, c-format
|
||||
msgid "Selection added to %s"
|
||||
msgid "The wakeup interval is %s, but must be 1h or less"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:134
|
||||
#, c-format
|
||||
msgid "One entry added to %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:135
|
||||
#, c-format
|
||||
msgid "%d entries added to %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:136
|
||||
#, c-format
|
||||
msgid "One entry removed from %s"
|
||||
msgid "Your account could not be connected"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:137
|
||||
#, c-format
|
||||
msgid "%d entries removed from %s"
|
||||
msgid "Changes successfully saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:138
|
||||
msgid "Account created"
|
||||
msgid "Album created"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:139
|
||||
msgid "Account saved"
|
||||
msgid "Album saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:140
|
||||
msgid "Account deleted"
|
||||
#, c-format
|
||||
msgid "Album %s deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:141
|
||||
msgid "Settings saved"
|
||||
msgid "Album contents cloned"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:142
|
||||
msgid "Password changed"
|
||||
msgid "File removed from stack"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:143
|
||||
#, c-format
|
||||
msgid "Import completed in %d s"
|
||||
msgid "File deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:144
|
||||
msgid "Import canceled"
|
||||
#, c-format
|
||||
msgid "Selection added to %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:145
|
||||
#, c-format
|
||||
msgid "Indexing completed in %d s"
|
||||
msgid "One entry added to %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:146
|
||||
msgid "Indexing originals..."
|
||||
#, c-format
|
||||
msgid "%d entries added to %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:147
|
||||
#, c-format
|
||||
msgid "Indexing files in %s"
|
||||
msgid "One entry removed from %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:148
|
||||
msgid "Indexing canceled"
|
||||
#, c-format
|
||||
msgid "%d entries removed from %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:149
|
||||
#, c-format
|
||||
msgid "Removed %d files and %d photos"
|
||||
msgid "Account created"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:150
|
||||
#, c-format
|
||||
msgid "Moving files from %s"
|
||||
msgid "Account saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:151
|
||||
#, c-format
|
||||
msgid "Copying files from %s"
|
||||
msgid "Account deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:152
|
||||
msgid "Labels deleted"
|
||||
msgid "Settings saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:153
|
||||
msgid "Label saved"
|
||||
msgid "Password changed"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:154
|
||||
msgid "Subject saved"
|
||||
#, c-format
|
||||
msgid "Import completed in %d s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:155
|
||||
msgid "Subject deleted"
|
||||
msgid "Import canceled"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:156
|
||||
msgid "Person saved"
|
||||
#, c-format
|
||||
msgid "Indexing completed in %d s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:157
|
||||
msgid "Person deleted"
|
||||
msgid "Indexing originals..."
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:158
|
||||
#, c-format
|
||||
msgid "%d files uploaded in %d s"
|
||||
msgid "Indexing files in %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:159
|
||||
msgid "Selection approved"
|
||||
msgid "Indexing canceled"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:160
|
||||
msgid "Selection archived"
|
||||
#, c-format
|
||||
msgid "Removed %d files and %d photos"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:161
|
||||
msgid "Selection restored"
|
||||
#, c-format
|
||||
msgid "Moving files from %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:162
|
||||
msgid "Selection marked as private"
|
||||
#, c-format
|
||||
msgid "Copying files from %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:163
|
||||
msgid "Albums deleted"
|
||||
msgid "Labels deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:164
|
||||
msgid "Label saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:165
|
||||
msgid "Subject saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:166
|
||||
msgid "Subject deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:167
|
||||
msgid "Person saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:168
|
||||
msgid "Person deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:169
|
||||
msgid "File uploaded"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:170
|
||||
#, c-format
|
||||
msgid "%d files uploaded in %d s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:171
|
||||
msgid "Processing upload..."
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:172
|
||||
msgid "Upload has been processed"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:173
|
||||
msgid "Selection approved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:174
|
||||
msgid "Selection archived"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:175
|
||||
msgid "Selection restored"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:176
|
||||
msgid "Selection marked as private"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:177
|
||||
msgid "Albums deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:178
|
||||
#, c-format
|
||||
msgid "Zip created in %d s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:165
|
||||
#: messages.go:179
|
||||
msgid "Permanently deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:166
|
||||
#: messages.go:180
|
||||
#, c-format
|
||||
msgid "%s has been restored"
|
||||
msgstr ""
|
||||
|
|
230
frontend/package-lock.json
generated
230
frontend/package-lock.json
generated
|
@ -2350,9 +2350,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.5.tgz",
|
||||
"integrity": "sha512-Bq7G3AErwe5A/Zki5fdD3O6+0zDChhg671NfPjtIcbtzDNZTv4NPKMRFr7gtYPG7y+B8uTiNK4Ngd9T0FTar6Q=="
|
||||
"version": "18.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.0.tgz",
|
||||
"integrity": "sha512-IOXCvVRToe7e0ny7HpT/X9Rb2RYtElG1a+VshjwT00HxrM2dWBApHQoqsI6WiY7Q03vdf2bCrIGzVrkF/5t10w=="
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
|
@ -2364,42 +2364,37 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.2.tgz",
|
||||
"integrity": "sha512-EDrLIPaPXOZqDjrkzxxbX7UlJSeQVgah3i0aA4pOSzmK9zq3BIh7/MZIQxED7slJByvKM4Gc6Hypyu2lJzh3SQ=="
|
||||
},
|
||||
"node_modules/@ungap/promise-all-settled": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz",
|
||||
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.2.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.40.tgz",
|
||||
"integrity": "sha512-2Dc3Stk0J/VyQ4OUr2yEC53kU28614lZS+bnrCbFSAIftBJ40g/2yQzf4mPBiFuqguMB7hyHaujdgZAQ67kZYA==",
|
||||
"version": "3.2.41",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.41.tgz",
|
||||
"integrity": "sha512-oA4mH6SA78DT+96/nsi4p9DX97PHcNROxs51lYk7gb9Z4BPKQ3Mh+BLn6CQZBw857Iuhu28BfMSRHAlPvD4vlw==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
"@vue/shared": "3.2.40",
|
||||
"@vue/shared": "3.2.41",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.2.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.40.tgz",
|
||||
"integrity": "sha512-OZCNyYVC2LQJy4H7h0o28rtk+4v+HMQygRTpmibGoG9wZyomQiS5otU7qo3Wlq5UfHDw2RFwxb9BJgKjVpjrQw==",
|
||||
"version": "3.2.41",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.41.tgz",
|
||||
"integrity": "sha512-xe5TbbIsonjENxJsYRbDJvthzqxLNk+tb3d/c47zgREDa/PCp6/Y4gC/skM4H6PIuX5DAxm7fFJdbjjUH2QTMw==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.2.40",
|
||||
"@vue/shared": "3.2.40"
|
||||
"@vue/compiler-core": "3.2.41",
|
||||
"@vue/shared": "3.2.41"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.2.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.40.tgz",
|
||||
"integrity": "sha512-tzqwniIN1fu1PDHC3CpqY/dPCfN/RN1thpBC+g69kJcrl7mbGiHKNwbA6kJ3XKKy8R6JLKqcpVugqN4HkeBFFg==",
|
||||
"version": "3.2.41",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.41.tgz",
|
||||
"integrity": "sha512-+1P2m5kxOeaxVmJNXnBskAn3BenbTmbxBxWOtBq3mQTCokIreuMULFantBUclP0+KnzNCMOvcnKinqQZmiOF8w==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
"@vue/compiler-core": "3.2.40",
|
||||
"@vue/compiler-dom": "3.2.40",
|
||||
"@vue/compiler-ssr": "3.2.40",
|
||||
"@vue/reactivity-transform": "3.2.40",
|
||||
"@vue/shared": "3.2.40",
|
||||
"@vue/compiler-core": "3.2.41",
|
||||
"@vue/compiler-dom": "3.2.41",
|
||||
"@vue/compiler-ssr": "3.2.41",
|
||||
"@vue/reactivity-transform": "3.2.41",
|
||||
"@vue/shared": "3.2.41",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.25.7",
|
||||
"postcss": "^8.1.10",
|
||||
|
@ -2407,12 +2402,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.2.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.40.tgz",
|
||||
"integrity": "sha512-80cQcgasKjrPPuKcxwuCx7feq+wC6oFl5YaKSee9pV3DNq+6fmCVwEEC3vvkf/E2aI76rIJSOYHsWSEIxK74oQ==",
|
||||
"version": "3.2.41",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.41.tgz",
|
||||
"integrity": "sha512-Y5wPiNIiaMz/sps8+DmhaKfDm1xgj6GrH99z4gq2LQenfVQcYXmHIOBcs5qPwl7jaW3SUQWjkAPKMfQemEQZwQ==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.2.40",
|
||||
"@vue/shared": "3.2.40"
|
||||
"@vue/compiler-dom": "3.2.41",
|
||||
"@vue/shared": "3.2.41"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/component-compiler-utils": {
|
||||
|
@ -2455,21 +2450,21 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity-transform": {
|
||||
"version": "3.2.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.40.tgz",
|
||||
"integrity": "sha512-HQUCVwEaacq6fGEsg2NUuGKIhUveMCjOk8jGHqLXPI2w6zFoPrlQhwWEaINTv5kkZDXKEnCijAp+4gNEHG03yw==",
|
||||
"version": "3.2.41",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.41.tgz",
|
||||
"integrity": "sha512-mK5+BNMsL4hHi+IR3Ft/ho6Za+L3FA5j8WvreJ7XzHrqkPq8jtF/SMo7tuc9gHjLDwKZX1nP1JQOKo9IEAn54A==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
"@vue/compiler-core": "3.2.40",
|
||||
"@vue/shared": "3.2.40",
|
||||
"@vue/compiler-core": "3.2.41",
|
||||
"@vue/shared": "3.2.41",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.25.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.2.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.40.tgz",
|
||||
"integrity": "sha512-0PLQ6RUtZM0vO3teRfzGi4ltLUO5aO+kLgwh4Um3THSR03rpQWLTuRCkuO5A41ITzwdWeKdPHtSARuPkoo5pCQ=="
|
||||
"version": "3.2.41",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.41.tgz",
|
||||
"integrity": "sha512-W9mfWLHmJhkfAmV+7gDjcHeAWALQtgGT3JErxULl0oz6R6+3ug91I7IErs93eCFhPCZPHBs4QJS7YWEV7A3sxw=="
|
||||
},
|
||||
"node_modules/@vvo/tzdb": {
|
||||
"version": "6.71.0",
|
||||
|
@ -3070,9 +3065,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.1.2.tgz",
|
||||
"integrity": "sha512-bznQyETwElsXl2RK7HLLwb5GPpOLlycxHCtrpDR/4RqqBzjARaOTo3jz4IgtntWUYee7Ne4S8UHd92VCuzPaWA==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
|
||||
"integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
|
@ -3503,9 +3498,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001419",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001419.tgz",
|
||||
"integrity": "sha512-aFO1r+g6R7TW+PNQxKzjITwLOyDhVRLjW0LcwS/HCZGUUKTGNp9+IwLC4xyDSZBygVL/mxaFR3HIV6wEKQuSzw==",
|
||||
"version": "1.0.30001420",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001420.tgz",
|
||||
"integrity": "sha512-OnyeJ9ascFA9roEj72ok2Ikp7PHJTKubtEJIQ/VK3fdsS50q4KWy+Z5X0A1/GswEItKX0ctAp8n4SYDE7wTu6A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
@ -4645,9 +4640,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.282",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.282.tgz",
|
||||
"integrity": "sha512-Dki0WhHNh/br/Xi1vAkueU5mtIc9XLHcMKB6tNfQKk+kPG0TEUjRh5QEMAUbRp30/rYNMFD1zKKvbVzwq/4wmg=="
|
||||
"version": "1.4.283",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.283.tgz",
|
||||
"integrity": "sha512-g6RQ9zCOV+U5QVHW9OpFR7rdk/V7xfopNXnyAamdpFgCHgZ1sjI8VuR1+zG2YG/TZk+tQ8mpNkug4P8FU0fuOA=="
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
|
@ -8538,11 +8533,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/mocha": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz",
|
||||
"integrity": "sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz",
|
||||
"integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==",
|
||||
"dependencies": {
|
||||
"@ungap/promise-all-settled": "1.1.2",
|
||||
"ansi-colors": "4.1.1",
|
||||
"browser-stdout": "1.3.1",
|
||||
"chokidar": "3.5.3",
|
||||
|
@ -11489,9 +11483,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.2.tgz",
|
||||
"integrity": "sha512-6fCnk4ARMPZN448+SQcnn1u8OHUC72puJcNtSgg2xS34Cu7br1gQ09YKkO1PFfDn/wyUE9ZgMAwosJed003+NQ==",
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz",
|
||||
"integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
|
@ -12170,9 +12164,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/ua-parser-js": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",
|
||||
"integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==",
|
||||
"version": "0.7.32",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.32.tgz",
|
||||
"integrity": "sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
@ -12382,15 +12376,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/util": {
|
||||
"version": "0.12.4",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz",
|
||||
"integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==",
|
||||
"version": "0.12.5",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"is-arguments": "^1.0.4",
|
||||
"is-generator-function": "^1.0.7",
|
||||
"is-typed-array": "^1.1.3",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"which-typed-array": "^1.1.2"
|
||||
}
|
||||
},
|
||||
|
@ -14877,9 +14870,9 @@
|
|||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.5.tgz",
|
||||
"integrity": "sha512-Bq7G3AErwe5A/Zki5fdD3O6+0zDChhg671NfPjtIcbtzDNZTv4NPKMRFr7gtYPG7y+B8uTiNK4Ngd9T0FTar6Q=="
|
||||
"version": "18.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.0.tgz",
|
||||
"integrity": "sha512-IOXCvVRToe7e0ny7HpT/X9Rb2RYtElG1a+VshjwT00HxrM2dWBApHQoqsI6WiY7Q03vdf2bCrIGzVrkF/5t10w=="
|
||||
},
|
||||
"@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
|
@ -14891,42 +14884,37 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.2.tgz",
|
||||
"integrity": "sha512-EDrLIPaPXOZqDjrkzxxbX7UlJSeQVgah3i0aA4pOSzmK9zq3BIh7/MZIQxED7slJByvKM4Gc6Hypyu2lJzh3SQ=="
|
||||
},
|
||||
"@ungap/promise-all-settled": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz",
|
||||
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="
|
||||
},
|
||||
"@vue/compiler-core": {
|
||||
"version": "3.2.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.40.tgz",
|
||||
"integrity": "sha512-2Dc3Stk0J/VyQ4OUr2yEC53kU28614lZS+bnrCbFSAIftBJ40g/2yQzf4mPBiFuqguMB7hyHaujdgZAQ67kZYA==",
|
||||
"version": "3.2.41",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.41.tgz",
|
||||
"integrity": "sha512-oA4mH6SA78DT+96/nsi4p9DX97PHcNROxs51lYk7gb9Z4BPKQ3Mh+BLn6CQZBw857Iuhu28BfMSRHAlPvD4vlw==",
|
||||
"requires": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
"@vue/shared": "3.2.40",
|
||||
"@vue/shared": "3.2.41",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-dom": {
|
||||
"version": "3.2.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.40.tgz",
|
||||
"integrity": "sha512-OZCNyYVC2LQJy4H7h0o28rtk+4v+HMQygRTpmibGoG9wZyomQiS5otU7qo3Wlq5UfHDw2RFwxb9BJgKjVpjrQw==",
|
||||
"version": "3.2.41",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.41.tgz",
|
||||
"integrity": "sha512-xe5TbbIsonjENxJsYRbDJvthzqxLNk+tb3d/c47zgREDa/PCp6/Y4gC/skM4H6PIuX5DAxm7fFJdbjjUH2QTMw==",
|
||||
"requires": {
|
||||
"@vue/compiler-core": "3.2.40",
|
||||
"@vue/shared": "3.2.40"
|
||||
"@vue/compiler-core": "3.2.41",
|
||||
"@vue/shared": "3.2.41"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-sfc": {
|
||||
"version": "3.2.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.40.tgz",
|
||||
"integrity": "sha512-tzqwniIN1fu1PDHC3CpqY/dPCfN/RN1thpBC+g69kJcrl7mbGiHKNwbA6kJ3XKKy8R6JLKqcpVugqN4HkeBFFg==",
|
||||
"version": "3.2.41",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.41.tgz",
|
||||
"integrity": "sha512-+1P2m5kxOeaxVmJNXnBskAn3BenbTmbxBxWOtBq3mQTCokIreuMULFantBUclP0+KnzNCMOvcnKinqQZmiOF8w==",
|
||||
"requires": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
"@vue/compiler-core": "3.2.40",
|
||||
"@vue/compiler-dom": "3.2.40",
|
||||
"@vue/compiler-ssr": "3.2.40",
|
||||
"@vue/reactivity-transform": "3.2.40",
|
||||
"@vue/shared": "3.2.40",
|
||||
"@vue/compiler-core": "3.2.41",
|
||||
"@vue/compiler-dom": "3.2.41",
|
||||
"@vue/compiler-ssr": "3.2.41",
|
||||
"@vue/reactivity-transform": "3.2.41",
|
||||
"@vue/shared": "3.2.41",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.25.7",
|
||||
"postcss": "^8.1.10",
|
||||
|
@ -14934,12 +14922,12 @@
|
|||
}
|
||||
},
|
||||
"@vue/compiler-ssr": {
|
||||
"version": "3.2.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.40.tgz",
|
||||
"integrity": "sha512-80cQcgasKjrPPuKcxwuCx7feq+wC6oFl5YaKSee9pV3DNq+6fmCVwEEC3vvkf/E2aI76rIJSOYHsWSEIxK74oQ==",
|
||||
"version": "3.2.41",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.41.tgz",
|
||||
"integrity": "sha512-Y5wPiNIiaMz/sps8+DmhaKfDm1xgj6GrH99z4gq2LQenfVQcYXmHIOBcs5qPwl7jaW3SUQWjkAPKMfQemEQZwQ==",
|
||||
"requires": {
|
||||
"@vue/compiler-dom": "3.2.40",
|
||||
"@vue/shared": "3.2.40"
|
||||
"@vue/compiler-dom": "3.2.41",
|
||||
"@vue/shared": "3.2.41"
|
||||
}
|
||||
},
|
||||
"@vue/component-compiler-utils": {
|
||||
|
@ -14975,21 +14963,21 @@
|
|||
}
|
||||
},
|
||||
"@vue/reactivity-transform": {
|
||||
"version": "3.2.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.40.tgz",
|
||||
"integrity": "sha512-HQUCVwEaacq6fGEsg2NUuGKIhUveMCjOk8jGHqLXPI2w6zFoPrlQhwWEaINTv5kkZDXKEnCijAp+4gNEHG03yw==",
|
||||
"version": "3.2.41",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.41.tgz",
|
||||
"integrity": "sha512-mK5+BNMsL4hHi+IR3Ft/ho6Za+L3FA5j8WvreJ7XzHrqkPq8jtF/SMo7tuc9gHjLDwKZX1nP1JQOKo9IEAn54A==",
|
||||
"requires": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
"@vue/compiler-core": "3.2.40",
|
||||
"@vue/shared": "3.2.40",
|
||||
"@vue/compiler-core": "3.2.41",
|
||||
"@vue/shared": "3.2.41",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.25.7"
|
||||
}
|
||||
},
|
||||
"@vue/shared": {
|
||||
"version": "3.2.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.40.tgz",
|
||||
"integrity": "sha512-0PLQ6RUtZM0vO3teRfzGi4ltLUO5aO+kLgwh4Um3THSR03rpQWLTuRCkuO5A41ITzwdWeKdPHtSARuPkoo5pCQ=="
|
||||
"version": "3.2.41",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.41.tgz",
|
||||
"integrity": "sha512-W9mfWLHmJhkfAmV+7gDjcHeAWALQtgGT3JErxULl0oz6R6+3ug91I7IErs93eCFhPCZPHBs4QJS7YWEV7A3sxw=="
|
||||
},
|
||||
"@vvo/tzdb": {
|
||||
"version": "6.71.0",
|
||||
|
@ -15441,9 +15429,9 @@
|
|||
"integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.1.2.tgz",
|
||||
"integrity": "sha512-bznQyETwElsXl2RK7HLLwb5GPpOLlycxHCtrpDR/4RqqBzjARaOTo3jz4IgtntWUYee7Ne4S8UHd92VCuzPaWA==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
|
||||
"integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
|
@ -15775,9 +15763,9 @@
|
|||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001419",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001419.tgz",
|
||||
"integrity": "sha512-aFO1r+g6R7TW+PNQxKzjITwLOyDhVRLjW0LcwS/HCZGUUKTGNp9+IwLC4xyDSZBygVL/mxaFR3HIV6wEKQuSzw=="
|
||||
"version": "1.0.30001420",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001420.tgz",
|
||||
"integrity": "sha512-OnyeJ9ascFA9roEj72ok2Ikp7PHJTKubtEJIQ/VK3fdsS50q4KWy+Z5X0A1/GswEItKX0ctAp8n4SYDE7wTu6A=="
|
||||
},
|
||||
"chai": {
|
||||
"version": "4.3.6",
|
||||
|
@ -16596,9 +16584,9 @@
|
|||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.4.282",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.282.tgz",
|
||||
"integrity": "sha512-Dki0WhHNh/br/Xi1vAkueU5mtIc9XLHcMKB6tNfQKk+kPG0TEUjRh5QEMAUbRp30/rYNMFD1zKKvbVzwq/4wmg=="
|
||||
"version": "1.4.283",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.283.tgz",
|
||||
"integrity": "sha512-g6RQ9zCOV+U5QVHW9OpFR7rdk/V7xfopNXnyAamdpFgCHgZ1sjI8VuR1+zG2YG/TZk+tQ8mpNkug4P8FU0fuOA=="
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
|
@ -19440,11 +19428,10 @@
|
|||
}
|
||||
},
|
||||
"mocha": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz",
|
||||
"integrity": "sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz",
|
||||
"integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==",
|
||||
"requires": {
|
||||
"@ungap/promise-all-settled": "1.1.2",
|
||||
"ansi-colors": "4.1.1",
|
||||
"browser-stdout": "1.3.1",
|
||||
"chokidar": "3.5.3",
|
||||
|
@ -21429,9 +21416,9 @@
|
|||
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="
|
||||
},
|
||||
"socket.io": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.2.tgz",
|
||||
"integrity": "sha512-6fCnk4ARMPZN448+SQcnn1u8OHUC72puJcNtSgg2xS34Cu7br1gQ09YKkO1PFfDn/wyUE9ZgMAwosJed003+NQ==",
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz",
|
||||
"integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==",
|
||||
"requires": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
|
@ -21930,9 +21917,9 @@
|
|||
}
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",
|
||||
"integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ=="
|
||||
"version": "0.7.32",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.32.tgz",
|
||||
"integrity": "sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.17.3",
|
||||
|
@ -22053,15 +22040,14 @@
|
|||
}
|
||||
},
|
||||
"util": {
|
||||
"version": "0.12.4",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz",
|
||||
"integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==",
|
||||
"version": "0.12.5",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"is-arguments": "^1.0.4",
|
||||
"is-generator-function": "^1.0.7",
|
||||
"is-typed-array": "^1.1.3",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"which-typed-array": "^1.1.2"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"watch": "webpack --watch",
|
||||
"build": "webpack --node-env=production",
|
||||
"trace": "webpack --stats-children",
|
||||
"debug": "webpack --stats-error-details",
|
||||
"lint": "eslint --cache src/ *.js",
|
||||
"fmt": "eslint --cache --fix src/ *.js .eslintrc.js",
|
||||
"test": "karma start",
|
||||
|
|
|
@ -23,23 +23,23 @@ Additional information can be found in our Developer Guide:
|
|||
|
||||
*/
|
||||
|
||||
import Photos from "pages/photos.vue";
|
||||
import Albums from "pages/albums.vue";
|
||||
import AlbumPhotos from "pages/album/photos.vue";
|
||||
import Places from "pages/places.vue";
|
||||
import Browse from "pages/files/browse.vue";
|
||||
import Errors from "pages/files/errors.vue";
|
||||
import Labels from "pages/labels.vue";
|
||||
import People from "pages/people.vue";
|
||||
import Files from "pages/files.vue";
|
||||
import Settings from "pages/settings.vue";
|
||||
import Login from "pages/login.vue";
|
||||
import Connect from "pages/connect.vue";
|
||||
import Discover from "pages/discover.vue";
|
||||
import About from "pages/about/about.vue";
|
||||
import Feedback from "pages/about/feedback.vue";
|
||||
import License from "pages/about/license.vue";
|
||||
import Help from "pages/help.vue";
|
||||
import Photos from "page/photos.vue";
|
||||
import Albums from "page/albums.vue";
|
||||
import AlbumPhotos from "page/album/photos.vue";
|
||||
import Places from "page/places.vue";
|
||||
import Browse from "page/library/browse.vue";
|
||||
import Errors from "page/library/errors.vue";
|
||||
import Labels from "page/labels.vue";
|
||||
import People from "page/people.vue";
|
||||
import Library from "page/library.vue";
|
||||
import Settings from "page/settings.vue";
|
||||
import Login from "page/login.vue";
|
||||
import Connect from "page/connect.vue";
|
||||
import Discover from "page/discover.vue";
|
||||
import About from "page/about/about.vue";
|
||||
import Feedback from "page/about/feedback.vue";
|
||||
import License from "page/about/license.vue";
|
||||
import Help from "page/help.vue";
|
||||
import { $gettext } from "common/vm";
|
||||
import { config, session } from "./session";
|
||||
|
||||
|
@ -319,25 +319,25 @@ export default [
|
|||
meta: { title: $gettext("People"), auth: true, background: "application-light" },
|
||||
},
|
||||
{
|
||||
name: "index",
|
||||
name: "library_index",
|
||||
path: "/index",
|
||||
component: Files,
|
||||
component: Library,
|
||||
meta: { title: $gettext("Library"), auth: true, background: "application-light" },
|
||||
props: { tab: "index" },
|
||||
props: { tab: "library_index" },
|
||||
},
|
||||
{
|
||||
name: "index_import",
|
||||
name: "library_import",
|
||||
path: "/import",
|
||||
component: Files,
|
||||
component: Library,
|
||||
meta: { title: $gettext("Library"), auth: true, background: "application-light" },
|
||||
props: { tab: "import" },
|
||||
props: { tab: "library_import" },
|
||||
},
|
||||
{
|
||||
name: "index_logs",
|
||||
name: "library_logs",
|
||||
path: "/logs",
|
||||
component: Files,
|
||||
component: Library,
|
||||
meta: { title: $gettext("Library"), auth: true, background: "application-light" },
|
||||
props: { tab: "logs" },
|
||||
props: { tab: "library_logs" },
|
||||
},
|
||||
{
|
||||
name: "settings",
|
||||
|
@ -352,8 +352,8 @@ export default [
|
|||
props: { tab: "settings-general" },
|
||||
},
|
||||
{
|
||||
name: "settings_files",
|
||||
path: "/settings/files",
|
||||
name: "settings_media",
|
||||
path: "/settings/media",
|
||||
component: Settings,
|
||||
meta: {
|
||||
title: $gettext("Settings"),
|
||||
|
@ -362,7 +362,7 @@ export default [
|
|||
settings: true,
|
||||
background: "application-light",
|
||||
},
|
||||
props: { tab: "settings-files" },
|
||||
props: { tab: "settings-media" },
|
||||
},
|
||||
{
|
||||
name: "settings_advanced",
|
||||
|
|
|
@ -209,6 +209,14 @@ export default class Session {
|
|||
return this.user;
|
||||
}
|
||||
|
||||
getUserUID() {
|
||||
if (this.user && this.user.UID) {
|
||||
return this.user.UID;
|
||||
} else {
|
||||
return "u000000000000001"; // Unknown.
|
||||
}
|
||||
}
|
||||
|
||||
loginRequired() {
|
||||
return !this.config.isPublic() && !this.isUser();
|
||||
}
|
||||
|
|
|
@ -387,7 +387,7 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile v-if="isMini && $config.feature('library')" :to="{ name: 'index' }" class="nav-library" @click.stop="">
|
||||
<v-list-tile v-if="isMini && $config.feature('library')" :to="{ name: 'library_index' }" class="nav-library" @click.stop="">
|
||||
<v-list-tile-action :title="$gettext('Library')">
|
||||
<v-icon>camera_roll</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
@ -401,7 +401,7 @@
|
|||
|
||||
<v-list-group v-if="!isMini && $config.feature('library')" prepend-icon="camera_roll" no-action>
|
||||
<template #activator>
|
||||
<v-list-tile :to="{ name: 'index' }" class="nav-library" @click.stop="">
|
||||
<v-list-tile :to="{ name: 'library_index' }" class="nav-library" @click.stop="">
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title class="p-flex-menuitem">
|
||||
<translate key="Library">Library</translate>
|
||||
|
@ -519,9 +519,12 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile v-show="auth && !isPublic && $config.feature('settings')" to="/settings/account" class="p-profile">
|
||||
<v-list-tile-avatar color="grey" size="36">
|
||||
<span class="white--text headline">{{ !!displayName ? displayName[0].toUpperCase() : "E" }}</span>
|
||||
<v-list-tile v-show="auth && !isPublic && $config.feature('settings')" class="p-profile" @click.stop="onAccount">
|
||||
<v-list-tile-avatar size="36">
|
||||
<v-img :src="user.getAvatarURL()"
|
||||
:alt="displayName" aspect-ratio="1"
|
||||
class="grey lighten-1 elevation-0 clickable"
|
||||
></v-img>
|
||||
</v-list-tile-avatar>
|
||||
|
||||
<v-list-tile-content>
|
||||
|
@ -532,13 +535,13 @@
|
|||
</v-list-tile-content>
|
||||
|
||||
<v-list-tile-action :title="$gettext('Logout')">
|
||||
<v-btn icon @click.stop.prevent="logout">
|
||||
<v-btn icon @click.stop.prevent="onLogout">
|
||||
<v-icon>power_settings_new</v-icon>
|
||||
</v-btn>
|
||||
</v-list-tile-action>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile v-show="isMini && auth && !isPublic" class="nav-logout" @click.stop.prevent="logout">
|
||||
<v-list-tile v-show="isMini && auth && !isPublic" class="nav-logout" @click.stop.prevent="onLogout">
|
||||
<v-list-tile-action :title="$gettext('Logout')">
|
||||
<v-icon>power_settings_new</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
@ -555,19 +558,15 @@
|
|||
<div class="menu-content grow-top-right">
|
||||
<div class="menu-icons">
|
||||
<a v-if="auth && !isPublic" href="#" :title="$gettext('Logout')" class="menu-action nav-logout"
|
||||
@click.prevent="logout">
|
||||
@click.prevent="onLogout">
|
||||
<v-icon>power_settings_new</v-icon>
|
||||
</a>
|
||||
<a href="#" :title="$gettext('Reload')" class="menu-action nav-reload" @click.prevent="reloadApp">
|
||||
<v-icon>refresh</v-icon>
|
||||
</a>
|
||||
<router-link v-if="!auth && !isPublic" :to="{ name: 'login' }" :title="$gettext('Login')"
|
||||
class="menu-action nav-login">
|
||||
<v-icon>login</v-icon>
|
||||
</router-link>
|
||||
<router-link v-if="auth && !routeName('index') && $config.feature('library') && $config.feature('logs')"
|
||||
:to="{ name: 'logs' }" :title="$gettext('Logs')" class="menu-action nav-logs">
|
||||
<v-icon>grading</v-icon>
|
||||
<router-link v-if="auth && $config.feature('account')"
|
||||
:to="{ name: 'settings_account' }" :title="$gettext('Account')" class="menu-action nav-account">
|
||||
<v-icon>admin_panel_settings</v-icon>
|
||||
</router-link>
|
||||
<router-link v-if="auth && $config.feature('settings') && !routeName('settings')" :to="{ name: 'settings' }"
|
||||
:title="$gettext('Settings')" class="menu-action nav-settings">
|
||||
|
@ -577,6 +576,10 @@
|
|||
class="menu-action nav-upload" @click.prevent="openUpload()">
|
||||
<v-icon>cloud_upload</v-icon>
|
||||
</a>
|
||||
<router-link v-if="!auth && !isPublic" :to="{ name: 'login' }" :title="$gettext('Login')"
|
||||
class="menu-action nav-login">
|
||||
<v-icon>login</v-icon>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="menu-actions">
|
||||
<div v-if="auth && !routeName('browse')&& $config.feature('search')" class="menu-action nav-search">
|
||||
|
@ -610,16 +613,16 @@
|
|||
<translate>Files</translate>
|
||||
</router-link>
|
||||
</div>
|
||||
<div v-if="auth && !routeName('index') && $config.feature('library')" class="menu-action nav-library">
|
||||
<router-link :to="{ name: 'index' }">
|
||||
<div v-if="auth && !routeName('library_index') && $config.feature('library')" class="menu-action nav-index">
|
||||
<router-link :to="{ name: 'library_index' }">
|
||||
<v-icon>camera_roll</v-icon>
|
||||
<translate>Index</translate>
|
||||
</router-link>
|
||||
</div>
|
||||
<div v-if="auth && $config.feature('sync') && !routeName('settings')" class="menu-action nav-sync">
|
||||
<router-link :to="{ name: 'settings_sync' }">
|
||||
<v-icon>sync</v-icon>
|
||||
<translate>Sync</translate>
|
||||
<div v-if="auth && !routeName('index') && $config.feature('library') && $config.feature('logs')" class="menu-action nav-logs">
|
||||
<router-link :to="{ name: 'library_logs' }">
|
||||
<v-icon>assignment</v-icon>
|
||||
<translate>Logs</translate>
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- div v-if="auth && $config.feature('account') && !routeName('settings')" class="menu-action nav-account">
|
||||
|
@ -660,6 +663,8 @@
|
|||
<script>
|
||||
import Album from "model/album";
|
||||
import Event from "pubsub-js";
|
||||
import Notify from "../common/notify";
|
||||
import User from "../model/user";
|
||||
|
||||
export default {
|
||||
name: "PNavigation",
|
||||
|
@ -708,6 +713,7 @@ export default {
|
|||
session: this.$session,
|
||||
config: this.$config.values,
|
||||
page: this.$config.page,
|
||||
user: this.$session.getUser(),
|
||||
reload: {
|
||||
dialog: false,
|
||||
},
|
||||
|
@ -808,7 +814,10 @@ export default {
|
|||
this.isMini = !this.isMini;
|
||||
localStorage.setItem('last_navigation_mode', `${this.isMini}`);
|
||||
},
|
||||
logout() {
|
||||
onAccount: function () {
|
||||
this.$router.push({name: "settings_account"});
|
||||
},
|
||||
onLogout() {
|
||||
this.$session.logout();
|
||||
},
|
||||
onIndex(ev) {
|
||||
|
|
|
@ -345,6 +345,7 @@ ol, ul {
|
|||
|
||||
.v-menu__content,
|
||||
.v-btn.v-btn--depressed:not(.v-btn--round):not(.v-btn--icon),
|
||||
.v-text-field.v-text-field--box > .v-input__control > .v-input__slot,
|
||||
.v-text-field.v-text-field--solo > .v-input__control > .v-input__slot,
|
||||
#photoprism .v-dialog .v-responsive.v-image,
|
||||
#photoprism .cards-view .result,
|
||||
|
@ -355,7 +356,9 @@ ol, ul {
|
|||
|
||||
.v-alert,
|
||||
#photoprism div.v-dialog > div.v-card,
|
||||
#photoprism div.v-dialog > div.v-card > .v-card__text .v-expansion-panel__container {
|
||||
#photoprism div.v-dialog > div.v-card > .v-card__text .v-expansion-panel__container,
|
||||
#photoprism div.v-dialog > form > div.v-card,
|
||||
#photoprism div.v-dialog > form > div.v-card > .v-card__text .v-expansion-panel__container {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
|
|
|
@ -76,4 +76,20 @@
|
|||
#photoprism main .auth-login footer p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Account Page */
|
||||
|
||||
.user-avatar {
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
max-width: 150px;
|
||||
max-height: 150px;
|
||||
overflow: hidden;
|
||||
}
|
|
@ -29,11 +29,57 @@ body.dark-theme .theme--light.v-list a:hover {
|
|||
background: rgba(255,255,255,0.3) !important;
|
||||
}
|
||||
|
||||
body.dark-theme .v-input input::placeholder {
|
||||
color: rgba(255,255,255,0.5) !important;
|
||||
}
|
||||
|
||||
.v-text-field>.v-input__control>.v-input__slot:after,
|
||||
.v-text-field>.v-input__control>.v-input__slot:before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*
|
||||
body.dark-theme .v-text-field.v-text-field--enclosed .v-text-field__details {
|
||||
padding-left: 0;
|
||||
}*/
|
||||
|
||||
.theme--light.v-list .v-list__tile--highlighted,
|
||||
.theme--light.v-list a:hover {
|
||||
background: rgba(155,155,155,0.3) !important;
|
||||
}
|
||||
|
||||
body.dark-theme #photoprism .map-control .theme--light.v-input:not(.v-input--is-disabled) i,
|
||||
body.dark-theme #photoprism .map-control .theme--light.v-input:not(.v-input--is-disabled) input {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
body.dark-theme #photoprism .theme--light.v-chip,
|
||||
body.dark-theme #photoprism .v-card .theme--light.v-text-field--box>.v-input__control>.v-input__slot,
|
||||
body.dark-theme #photoprism .v-card .theme--light.v-text-field--solo>.v-input__control>.v-input__slot {
|
||||
background: #00000033;
|
||||
}
|
||||
|
||||
body.dark-theme #photoprism,
|
||||
body.dark-theme #photoprism .p-page a,
|
||||
body.dark-theme #photoprism .v-datatable a,
|
||||
body.dark-theme #photoprism .theme--light.v-expansion-panel .v-expansion-panel__container,
|
||||
body.dark-theme #photoprism .theme--light.v-tabs__bar .v-tabs__div,
|
||||
body.dark-theme #photoprism .theme--light {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-theme #photoprism .theme--light.v-label {
|
||||
color: #ffffffaa;
|
||||
}
|
||||
|
||||
body.dark-theme #photoprism .theme--light.v-select .v-select__selections {
|
||||
color: #ffffffee;
|
||||
}
|
||||
|
||||
#photoprism .v-select.v-text-field--box .v-select__selections {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
/* Exceptions */
|
||||
|
||||
#photoprism .theme--light.v-text-field--solo.background-inherit>.v-input__control>.v-input__slot {
|
||||
|
|
135
frontend/src/dialog/account/password.vue
Normal file
135
frontend/src/dialog/account/password.vue
Normal file
|
@ -0,0 +1,135 @@
|
|||
<template>
|
||||
<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 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 xs12 class="px-2 py-1">
|
||||
<v-text-field
|
||||
v-model="oldPassword"
|
||||
hide-details required box flat
|
||||
type="password"
|
||||
:disabled="busy"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Current Password')"
|
||||
class="input-current-password"
|
||||
color="secondary-dark"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 class="px-2 py-1">
|
||||
<v-text-field
|
||||
v-model="newPassword"
|
||||
required counter persistent-hint box flat
|
||||
type="password"
|
||||
:disabled="busy"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('New Password')"
|
||||
class="input-new-password"
|
||||
color="secondary-dark"
|
||||
:hint="$gettext('Must have at least 8 characters.')"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 class="px-2 py-1">
|
||||
<v-text-field
|
||||
v-model="confirmPassword"
|
||||
required counter persistent-hint box flat
|
||||
type="password"
|
||||
:disabled="busy"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Retype Password')"
|
||||
class="input-retype-password"
|
||||
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-form>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'PAccountPasswordDialog',
|
||||
props: {
|
||||
show: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
busy: false,
|
||||
isDemo: this.$config.get("demo"),
|
||||
isPublic: this.$config.get("public"),
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
rtl: this.$rtl,
|
||||
};
|
||||
},
|
||||
computed: {},
|
||||
created() {
|
||||
if(this.isPublic && !this.isDemo) {
|
||||
this.$emit('cancel');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
disabled() {
|
||||
return (this.isDemo || this.busy || this.oldPassword === "" || this.newPassword.length < 8 || (this.newPassword !== this.confirmPassword));
|
||||
},
|
||||
confirm() {
|
||||
this.busy = true;
|
||||
this.$session.getUser().changePassword(this.oldPassword, this.newPassword).then(() => {
|
||||
this.$notify.success(this.$gettext("Password changed"));
|
||||
this.$emit('confirm');
|
||||
}).finally(() => {
|
||||
this.busy = false;
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('cancel');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -20,7 +20,7 @@
|
|||
<v-layout row wrap>
|
||||
<v-flex v-if="album.Type !== 'month'" xs12 pa-2>
|
||||
<v-text-field v-model="model.Title"
|
||||
hide-details autofocus
|
||||
hide-details autofocus box flat
|
||||
:rules="[titleRule]"
|
||||
:label="$gettext('Name')"
|
||||
:disabled="disabled"
|
||||
|
@ -31,7 +31,7 @@
|
|||
</v-flex>
|
||||
<v-flex xs12 pa-2>
|
||||
<v-text-field v-model="model.Location"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
:label="$gettext('Location')"
|
||||
:disabled="disabled"
|
||||
color="secondary-dark"
|
||||
|
@ -41,7 +41,7 @@
|
|||
<v-flex xs12 pa-2>
|
||||
<v-textarea :key="growDesc" v-model="model.Description"
|
||||
auto-grow
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
:label="$gettext('Description')"
|
||||
:rows="1"
|
||||
|
@ -51,7 +51,7 @@
|
|||
</v-textarea>
|
||||
</v-flex>
|
||||
<v-flex xs12 md6 pa-2>
|
||||
<v-combobox v-model="model.Category" hide-details
|
||||
<v-combobox v-model="model.Category" hide-details box flat
|
||||
:search-input.sync="model.Category"
|
||||
:items="categories"
|
||||
:disabled="disabled"
|
||||
|
@ -67,7 +67,7 @@
|
|||
<v-select
|
||||
v-model="model.Order"
|
||||
:label="$gettext('Sort Order')"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
:items="sorting"
|
||||
:disabled="disabled"
|
||||
item-value="value"
|
||||
|
@ -78,7 +78,7 @@
|
|||
</v-layout>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pt-0">
|
||||
<v-card-actions class="pt-0 px-3">
|
||||
<v-layout row wrap class="pa-2">
|
||||
<v-flex xs12 text-xs-right>
|
||||
<v-btn depressed color="secondary-light"
|
||||
|
|
|
@ -23,9 +23,9 @@ Additional information can be found in our Developer Guide:
|
|||
|
||||
*/
|
||||
|
||||
import PAccountAddDialog from "dialog/service/add.vue";
|
||||
import PAccountRemoveDialog from "dialog/service/remove.vue";
|
||||
import PAccountEditDialog from "dialog/service/edit.vue";
|
||||
import PServiceAddDialog from "dialog/service/add.vue";
|
||||
import PServiceRemoveDialog from "dialog/service/remove.vue";
|
||||
import PServiceEditDialog from "dialog/service/edit.vue";
|
||||
import PPhotoArchiveDialog from "dialog/photo/archive.vue";
|
||||
import PPhotoAlbumDialog from "dialog/photo/album.vue";
|
||||
import PPhotoEditDialog from "dialog/photo/edit.vue";
|
||||
|
@ -47,9 +47,9 @@ import PConfirmDialog from "dialog/confirm.vue";
|
|||
const dialogs = {};
|
||||
|
||||
dialogs.install = (Vue) => {
|
||||
Vue.component("PAccountAddDialog", PAccountAddDialog);
|
||||
Vue.component("PAccountRemoveDialog", PAccountRemoveDialog);
|
||||
Vue.component("PAccountEditDialog", PAccountEditDialog);
|
||||
Vue.component("PServiceAddDialog", PServiceAddDialog);
|
||||
Vue.component("PServiceRemoveDialog", PServiceRemoveDialog);
|
||||
Vue.component("PServiceEditDialog", PServiceEditDialog);
|
||||
Vue.component("PPhotoArchiveDialog", PPhotoArchiveDialog);
|
||||
Vue.component("PPhotoAlbumDialog", PPhotoAlbumDialog);
|
||||
Vue.component("PPhotoEditDialog", PPhotoEditDialog);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<v-dialog :value="show" lazy persistent max-width="350" class="p-photo-album-dialog" @keydown.esc="cancel">
|
||||
<v-card raised elevation="24">
|
||||
<v-container fluid class="pb-2 pr-2 pl-2">
|
||||
<v-card-text class="pt-3 pl-0 pr-2">
|
||||
<v-layout row wrap>
|
||||
<v-flex xs3 text-xs-center>
|
||||
<v-icon size="56" color="secondary-dark lighten-1">photo_album</v-icon>
|
||||
|
@ -15,30 +15,33 @@
|
|||
:items="items"
|
||||
:search-input.sync="search"
|
||||
:loading="loading"
|
||||
hide-details
|
||||
hide-no-data
|
||||
hide-no-data hide-details box flat
|
||||
item-text="Title"
|
||||
item-value="UID"
|
||||
:label="$gettext('Album Name')"
|
||||
color="secondary-dark"
|
||||
flat solo
|
||||
class="input-album"
|
||||
@keyup.enter.native="confirm"
|
||||
>
|
||||
</v-autocomplete>
|
||||
</v-flex>
|
||||
<v-flex xs12 text-xs-right class="pt-3">
|
||||
<v-btn depressed color="secondary-light" class="action-cancel" @click.stop="cancel">
|
||||
</v-layout>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pt-1 pb-2 px-2">
|
||||
<v-layout row wrap class="pa-0">
|
||||
<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 color="primary-button" depressed dark class="action-confirm"
|
||||
<v-btn depressed color="primary-button"
|
||||
class="action-confirm white--text compact mr-0"
|
||||
@click.stop="confirm">
|
||||
<span v-if="!album">{{ labels.createAlbum }}</span>
|
||||
<span v-else>{{ labels.addToAlbum }}</span>
|
||||
</v-btn>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
@ -129,7 +132,7 @@ export default {
|
|||
this.$nextTick(() => this.$refs.input.focus());
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
})
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -109,11 +109,11 @@
|
|||
</template>
|
||||
<script>
|
||||
import Photo from "model/photo";
|
||||
import PhotoDetails from "details.vue";
|
||||
import PhotoLabels from "labels.vue";
|
||||
import PhotoPeople from "people.vue";
|
||||
import PhotoFiles from "files.vue";
|
||||
import PhotoInfo from "info.vue";
|
||||
import PhotoDetails from "./edit/details.vue";
|
||||
import PhotoLabels from "./edit/labels.vue";
|
||||
import PhotoPeople from "./edit/people.vue";
|
||||
import PhotoFiles from "./edit/files.vue";
|
||||
import PhotoInfo from "./edit/info.vue";
|
||||
import Event from "pubsub-js";
|
||||
|
||||
export default {
|
||||
|
@ -126,7 +126,10 @@ export default {
|
|||
'p-tab-photo-info': PhotoInfo,
|
||||
},
|
||||
props: {
|
||||
index: Number,
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
show: Boolean,
|
||||
selection: {
|
||||
type: Array,
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
:append-icon="model.TitleSrc === 'manual' ? 'check' : ''"
|
||||
:disabled="disabled"
|
||||
:rules="[textRule]"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
:label="$gettext('Title')"
|
||||
placeholder=""
|
||||
color="secondary-dark"
|
||||
|
@ -47,7 +47,7 @@
|
|||
:error="invalidDate"
|
||||
:label="$gettext('Day')"
|
||||
browser-autocomplete="off"
|
||||
hide-details hide-no-data
|
||||
hide-details box flat hide-no-data
|
||||
color="secondary-dark"
|
||||
:items="options.Days()"
|
||||
class="input-day"
|
||||
|
@ -62,7 +62,7 @@
|
|||
:error="invalidDate"
|
||||
:label="$gettext('Month')"
|
||||
browser-autocomplete="off"
|
||||
hide-details hide-no-data
|
||||
hide-details box flat hide-no-data
|
||||
color="secondary-dark"
|
||||
:items="options.MonthsShort()"
|
||||
class="input-month"
|
||||
|
@ -77,7 +77,7 @@
|
|||
:error="invalidDate"
|
||||
:label="$gettext('Year')"
|
||||
browser-autocomplete="off"
|
||||
hide-details hide-no-data
|
||||
hide-details box flat hide-no-data
|
||||
color="secondary-dark"
|
||||
:items="options.Years()"
|
||||
class="input-year"
|
||||
|
@ -94,7 +94,7 @@
|
|||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
return-masked-value mask="##:##:##"
|
||||
color="secondary-dark"
|
||||
class="input-local-time"
|
||||
|
@ -107,7 +107,7 @@
|
|||
:disabled="disabled"
|
||||
:label="$gettext('Time Zone')"
|
||||
browser-autocomplete="off"
|
||||
hide-details hide-no-data
|
||||
hide-details box flat hide-no-data
|
||||
color="secondary-dark"
|
||||
item-value="ID"
|
||||
item-text="Name"
|
||||
|
@ -123,7 +123,7 @@
|
|||
:append-icon="model.PlaceSrc === 'manual' ? 'check' : ''"
|
||||
:disabled="disabled"
|
||||
:readonly="!!(model.Lat || model.Lng)"
|
||||
:label="$gettext('Country')" hide-details
|
||||
:label="$gettext('Country')" hide-details box flat
|
||||
hide-no-data
|
||||
browser-autocomplete="off"
|
||||
color="secondary-dark"
|
||||
|
@ -138,7 +138,7 @@
|
|||
<v-text-field
|
||||
v-model="model.Altitude"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
|
@ -154,7 +154,7 @@
|
|||
v-model="model.Lat"
|
||||
:append-icon="model.PlaceSrc === 'manual' ? 'check' : ''"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
|
@ -170,7 +170,7 @@
|
|||
v-model="model.Lng"
|
||||
:append-icon="model.PlaceSrc === 'manual' ? 'check' : ''"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
|
@ -188,7 +188,7 @@
|
|||
:disabled="disabled"
|
||||
:label="$gettext('Camera')"
|
||||
browser-autocomplete="off"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
color="secondary-dark"
|
||||
item-value="ID"
|
||||
item-text="Name"
|
||||
|
@ -201,7 +201,7 @@
|
|||
<v-text-field
|
||||
v-model="model.Iso"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
|
@ -216,7 +216,7 @@
|
|||
<v-text-field
|
||||
v-model="model.Exposure"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
|
@ -234,7 +234,7 @@
|
|||
:disabled="disabled"
|
||||
:label="$gettext('Lens')"
|
||||
browser-autocomplete="off"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
color="secondary-dark"
|
||||
item-value="ID"
|
||||
item-text="Name"
|
||||
|
@ -247,7 +247,7 @@
|
|||
<v-text-field
|
||||
v-model="model.FNumber"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
|
@ -262,7 +262,7 @@
|
|||
<v-text-field
|
||||
v-model="model.FocalLength"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
:label="$gettext('Focal Length')"
|
||||
placeholder=""
|
||||
|
@ -277,7 +277,7 @@
|
|||
:append-icon="model.Details.SubjectSrc === 'manual' ? 'check' : ''"
|
||||
:disabled="disabled"
|
||||
:rules="[textRule]"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
auto-grow
|
||||
:label="$gettext('Subject')"
|
||||
|
@ -294,7 +294,7 @@
|
|||
:append-icon="model.Details.ArtistSrc === 'manual' ? 'check' : ''"
|
||||
:disabled="disabled"
|
||||
:rules="[textRule]"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
:label="$gettext('Artist')"
|
||||
placeholder=""
|
||||
|
@ -309,7 +309,7 @@
|
|||
:append-icon="model.Details.CopyrightSrc === 'manual' ? 'check' : ''"
|
||||
:disabled="disabled"
|
||||
:rules="[textRule]"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
:label="$gettext('Copyright')"
|
||||
placeholder=""
|
||||
|
@ -324,7 +324,7 @@
|
|||
:append-icon="model.Details.LicenseSrc === 'manual' ? 'check' : ''"
|
||||
:disabled="disabled"
|
||||
:rules="[textRule]"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
auto-grow
|
||||
:label="$gettext('License')"
|
||||
|
@ -340,7 +340,7 @@
|
|||
v-model="model.Description"
|
||||
:append-icon="model.DescriptionSrc === 'manual' ? 'check' : ''"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
auto-grow
|
||||
:label="$gettext('Description')"
|
||||
|
@ -356,7 +356,7 @@
|
|||
v-model="model.Details.Keywords"
|
||||
:append-icon="model.Details.KeywordsSrc === 'manual' ? 'check' : ''"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
auto-grow
|
||||
:label="$gettext('Keywords')"
|
||||
|
@ -372,7 +372,7 @@
|
|||
v-model="model.Details.Notes"
|
||||
:append-icon="model.Details.NotesSrc === 'manual' ? 'check' : ''"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
auto-grow
|
||||
:label="$gettext('Notes')"
|
||||
|
@ -411,13 +411,20 @@
|
|||
<script>
|
||||
import countries from "options/countries.json";
|
||||
import Thumb from "model/thumb";
|
||||
import Photo from "model/photo";
|
||||
import * as options from "options/options";
|
||||
|
||||
export default {
|
||||
name: 'PTabPhotoDetails',
|
||||
props: {
|
||||
model: Object,
|
||||
uid: String,
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => new Photo(false),
|
||||
},
|
||||
uid: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
|
@ -1,19 +1,21 @@
|
|||
<template>
|
||||
<v-dialog :value="show" lazy persistent max-width="500" class="p-account-create-dialog" @keydown.esc="cancel">
|
||||
<v-card raised elevation="24">
|
||||
<v-card-title primary-title>
|
||||
<div>
|
||||
<h3 class="headline mx-2 my-0">
|
||||
<translate>Add Server</translate>
|
||||
</h3>
|
||||
</div>
|
||||
<v-card-title primary-title class="pa-2">
|
||||
<v-layout row wrap>
|
||||
<v-flex xs12 class="pa-2">
|
||||
<h3 class="headline pa-0">
|
||||
<translate>Add Account</translate>
|
||||
</h3>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card-title>
|
||||
<v-card-text class="pt-0">
|
||||
<v-card-text class="pb-0 pt-0 px-2">
|
||||
<v-layout row wrap>
|
||||
<v-flex xs12 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="model.AccURL"
|
||||
hide-details autofocus
|
||||
hide-details autofocus box flat
|
||||
:label="$gettext('Service URL')"
|
||||
placeholder="https://www.example.com/"
|
||||
color="secondary-dark"
|
||||
|
@ -24,7 +26,7 @@
|
|||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="model.AccUser"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
:label="$gettext('Username')"
|
||||
placeholder="optional"
|
||||
color="secondary-dark"
|
||||
|
@ -35,7 +37,7 @@
|
|||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="model.AccPass"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Password')"
|
||||
|
@ -46,22 +48,26 @@
|
|||
@click:append="showPassword = !showPassword"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12 text-xs-left class="pa-2 caption">
|
||||
</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-left class="caption">
|
||||
<translate>Note: Only WebDAV servers, like Nextcloud or PhotoPrism, can be configured as remote service for backup and file upload.</translate>
|
||||
<translate>Support for additional services, like Google Drive, will be added over time.</translate>
|
||||
</v-flex>
|
||||
<v-flex xs12 text-xs-right class="px-2 pt-2 pb-0">
|
||||
<v-flex xs12 text-xs-right class="pt-2">
|
||||
<v-btn depressed color="secondary-light" class="action-cancel mr-2"
|
||||
@click.stop="cancel">
|
||||
<span>{{ label.cancel }}</span>
|
||||
</v-btn>
|
||||
<v-btn depressed dark color="primary-button" class="action-confirm ma-0"
|
||||
<v-btn depressed dark color="primary-button" class="action-confirm compact ma-0"
|
||||
@click.stop="confirm">
|
||||
<span>{{ label.confirm }}</span>
|
||||
</v-btn>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card-text>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<v-dialog :value="show" lazy persistent max-width="500" class="p-account-edit-dialog" @keydown.esc="cancel">
|
||||
<v-card raised elevation="24">
|
||||
<v-card-title primary-title>
|
||||
<v-layout v-if="scope === 'sharing'" row wrap>
|
||||
<v-card-title primary-title class="pa-2">
|
||||
<v-layout v-if="scope === 'sharing'" row wrap class="py-2 pr-0 pl-2">
|
||||
<v-flex xs9>
|
||||
<h3 class="headline mx-2 my-0">{{ $gettext('Manual Upload') }}</h3>
|
||||
<h3 class="headline ma-0 pa-0">{{ $gettext('Manual Upload') }}</h3>
|
||||
</v-flex>
|
||||
<v-flex xs3 text-xs-right>
|
||||
<v-switch
|
||||
|
@ -12,9 +12,8 @@
|
|||
color="secondary-dark"
|
||||
:true-value="true"
|
||||
:false-value="false"
|
||||
:label="model.AccShare ? $gettext('Enabled') : $gettext('Disabled')"
|
||||
:disabled="model.AccType !== 'webdav'"
|
||||
class="ma-0 hidden-xs-only"
|
||||
class="ma-0 hidden-xs-only float-right"
|
||||
hide-details
|
||||
></v-switch>
|
||||
<v-switch
|
||||
|
@ -23,14 +22,14 @@
|
|||
:true-value="true"
|
||||
:false-value="false"
|
||||
:disabled="model.AccType !== 'webdav'"
|
||||
class="ma-0 hidden-sm-and-up"
|
||||
class="ma-0 hidden-sm-and-up float-right"
|
||||
hide-details
|
||||
></v-switch>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
<v-layout v-else-if="scope === 'sync'" row wrap>
|
||||
<v-layout v-else-if="scope === 'sync'" row wrap class="pa-2">
|
||||
<v-flex xs9>
|
||||
<h3 class="headline mx-2 my-0">{{ $gettext('Remote Sync') }}</h3>
|
||||
<h3 class="headline ma-0 pa-0">{{ $gettext('Remote Sync') }}</h3>
|
||||
</v-flex>
|
||||
<v-flex xs3 text-xs-right>
|
||||
<v-switch
|
||||
|
@ -38,10 +37,9 @@
|
|||
color="secondary-dark"
|
||||
:true-value="true"
|
||||
:false-value="false"
|
||||
:label="model.AccSync ? $gettext('Enabled') : $gettext('Disabled')"
|
||||
:disabled="model.AccType !== 'webdav'"
|
||||
class="mt-0 hidden-xs-only"
|
||||
hide-details
|
||||
class="mt-0 hidden-xs-only float-right"
|
||||
hide-details box flat
|
||||
></v-switch>
|
||||
<v-switch
|
||||
v-model="model.AccSync"
|
||||
|
@ -49,14 +47,14 @@
|
|||
:true-value="true"
|
||||
:false-value="false"
|
||||
:disabled="model.AccType !== 'webdav'"
|
||||
class="mt-0 hidden-sm-and-up"
|
||||
hide-details
|
||||
class="mt-0 hidden-sm-and-up float-right"
|
||||
hide-details box flat
|
||||
></v-switch>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
<v-layout v-else row wrap>
|
||||
<v-layout v-else row wrap class="pt-2 pr-0 pl-2">
|
||||
<v-flex xs10>
|
||||
<h3 class="headline mx-2 my-0">{{ $gettext('Edit Account') }}</h3>
|
||||
<h3 class="headline ma-0 pa-0">{{ $gettext('Edit Account') }}</h3>
|
||||
</v-flex>
|
||||
<v-flex xs2 text-xs-right>
|
||||
<v-btn icon flat :ripple="false"
|
||||
|
@ -67,13 +65,12 @@
|
|||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card-title>
|
||||
<v-card-text class="pt-0">
|
||||
<v-card-text class="py-0 px-2">
|
||||
<v-layout v-if="scope === 'sharing'" row wrap>
|
||||
<v-flex xs12 class="pa-2">
|
||||
<v-autocomplete
|
||||
v-model="model.SharePath"
|
||||
color="secondary-dark" hide-details hide-no-data
|
||||
flat
|
||||
color="secondary-dark" hide-details hide-no-data box flat
|
||||
browser-autocomplete="off"
|
||||
hint="Folder"
|
||||
:search-input.sync="search"
|
||||
|
@ -92,7 +89,7 @@
|
|||
:disabled="!model.AccShare"
|
||||
:label="$gettext('Size')"
|
||||
browser-autocomplete="off"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
color="secondary-dark"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
|
@ -105,7 +102,7 @@
|
|||
:disabled="!model.AccShare"
|
||||
:label="$gettext('Expires')"
|
||||
browser-autocomplete="off"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
color="secondary-dark"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
|
@ -117,8 +114,7 @@
|
|||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-autocomplete
|
||||
v-model="model.SyncPath"
|
||||
color="secondary-dark" hide-details hide-no-data
|
||||
flat
|
||||
color="secondary-dark" hide-details hide-no-data box flat
|
||||
browser-autocomplete="off"
|
||||
:hint="$gettext('Folder')"
|
||||
:search-input.sync="search"
|
||||
|
@ -137,7 +133,7 @@
|
|||
:disabled="!model.AccSync"
|
||||
:label="$gettext('Interval')"
|
||||
browser-autocomplete="off"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
color="secondary-dark"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
|
@ -148,7 +144,7 @@
|
|||
<v-checkbox
|
||||
v-model="model.SyncDownload"
|
||||
:disabled="!model.AccSync || readonly"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
color="secondary-dark"
|
||||
on-icon="radio_button_checked"
|
||||
off-icon="radio_button_unchecked"
|
||||
|
@ -160,7 +156,7 @@
|
|||
<v-checkbox
|
||||
v-model="model.SyncFilenames"
|
||||
:disabled="!model.AccSync"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
color="secondary-dark"
|
||||
:label="$gettext('Preserve filenames')"
|
||||
></v-checkbox>
|
||||
|
@ -169,7 +165,7 @@
|
|||
<v-checkbox
|
||||
v-model="model.SyncUpload"
|
||||
:disabled="!model.AccSync"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
color="secondary-dark"
|
||||
on-icon="radio_button_checked"
|
||||
off-icon="radio_button_unchecked"
|
||||
|
@ -181,17 +177,17 @@
|
|||
<v-checkbox
|
||||
v-model="model.SyncRaw"
|
||||
:disabled="!model.AccSync"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
color="secondary-dark"
|
||||
:label="$gettext('Sync raw and video files')"
|
||||
></v-checkbox>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
<v-layout v-else row wrap>
|
||||
<v-layout v-else row wrap class="pt-0">
|
||||
<v-flex xs12 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="model.AccName"
|
||||
hide-details autofocus
|
||||
hide-details autofocus box flat
|
||||
browser-autocomplete="off"
|
||||
:label="$gettext('Name')"
|
||||
placeholder=""
|
||||
|
@ -202,7 +198,7 @@
|
|||
<v-flex xs12 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="model.AccURL"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
:label="$gettext('Service URL')"
|
||||
placeholder="https://www.example.com/"
|
||||
|
@ -212,7 +208,7 @@
|
|||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="model.AccUser"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
:label="$gettext('Username')"
|
||||
placeholder="optional"
|
||||
|
@ -222,7 +218,7 @@
|
|||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="model.AccPass"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
:label="$gettext('Password')"
|
||||
placeholder="optional"
|
||||
|
@ -235,7 +231,7 @@
|
|||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="model.AccKey"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
:label="$gettext('API Key')"
|
||||
placeholder="optional"
|
||||
|
@ -248,7 +244,7 @@
|
|||
v-model="model.AccType"
|
||||
:label="$gettext('Type')"
|
||||
browser-autocomplete="off"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
color="secondary-dark"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
|
@ -260,7 +256,7 @@
|
|||
v-model="model.AccTimeout"
|
||||
:label="$gettext('Timeout')"
|
||||
browser-autocomplete="off"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
color="secondary-dark"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
|
@ -272,7 +268,7 @@
|
|||
v-model="model.RetryLimit"
|
||||
:label="$gettext('Retry Limit')"
|
||||
browser-autocomplete="off"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
color="secondary-dark"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
|
@ -280,19 +276,21 @@
|
|||
</v-select>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
<v-layout row wrap>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pt-0 pb-2 px-2">
|
||||
<v-layout row wrap class="pa-2">
|
||||
<v-flex xs12 text-xs-right class="pt-3 pb-0">
|
||||
<v-btn depressed color="secondary-light" class="action-cancel"
|
||||
@click.stop="cancel">
|
||||
<translate>Cancel</translate>
|
||||
</v-btn>
|
||||
<v-btn depressed dark color="primary-button" class="action-save"
|
||||
<v-btn depressed dark color="primary-button" class="action-save compact"
|
||||
@click.stop="save">
|
||||
<translate>Save</translate>
|
||||
</v-btn>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card-text>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
@ -303,7 +301,10 @@ export default {
|
|||
name: 'PAccountEditDialog',
|
||||
props: {
|
||||
show: Boolean,
|
||||
scope: String,
|
||||
scope: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
browser-autocomplete="off"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
readonly
|
||||
color="secondary-dark"
|
||||
class="input-url"
|
||||
|
@ -51,7 +51,7 @@
|
|||
v-model="link.Expires"
|
||||
:label="expires(link)"
|
||||
browser-autocomplete="off"
|
||||
hide-details
|
||||
hide-details box flat
|
||||
color="secondary-dark"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
|
@ -93,7 +93,7 @@
|
|||
</v-btn>
|
||||
</v-flex>
|
||||
<v-flex xs6 :text-xs-right="!rtl" :text-xs-left="rtl" class="pa-2">
|
||||
<v-btn depressed dark color="primary-button" class="ma-0 action-save"
|
||||
<v-btn depressed dark color="primary-button" class="ma-0 compact action-save"
|
||||
@click.stop.exact="update(link)">
|
||||
<translate>Save</translate>
|
||||
</v-btn>
|
||||
|
@ -112,7 +112,7 @@
|
|||
<translate>Alternatively, you can upload files directly to WebDAV servers like Nextcloud.</translate>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pt-0">
|
||||
<v-card-actions class="pt-0 px-3">
|
||||
<v-layout row wrap class="pa-2">
|
||||
<v-flex xs6>
|
||||
<v-btn depressed color="secondary-light" class="action-webdav"
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<v-select
|
||||
v-model="service"
|
||||
color="secondary-dark" hide-details hide-no-data
|
||||
flat
|
||||
box flat
|
||||
:label="$gettext('Account')"
|
||||
item-text="AccName"
|
||||
item-value="ID"
|
||||
|
@ -35,7 +35,7 @@
|
|||
<v-autocomplete
|
||||
v-model="path"
|
||||
color="secondary-dark" hide-details hide-no-data
|
||||
flat
|
||||
box flat
|
||||
browser-autocomplete="off"
|
||||
hint="Folder"
|
||||
:search-input.sync="search"
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<translate>Feel free to contact us at hello@photoprism.app if you have any questions.</translate>
|
||||
</p>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pt-0">
|
||||
<v-card-actions class="pt-0 px-3">
|
||||
<v-layout row wrap class="px-2">
|
||||
<v-flex xs12 sm4 text-xs-right text-sm-left class="py-2">
|
||||
<v-btn depressed color="secondary-light"
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
</v-toolbar>
|
||||
<v-container grid-list-xs ext-xs-left fluid>
|
||||
<v-form ref="form" class="p-photo-upload" lazy-validation dense @submit.prevent="submit">
|
||||
<input ref="upload" type="file" multiple class="d-none input-upload" @change.stop="upload()">
|
||||
<input ref="upload" type="file" multiple class="d-none input-upload" @change.stop="onUpload()">
|
||||
|
||||
<v-container fluid>
|
||||
<p class="subheading">
|
||||
|
@ -74,7 +74,7 @@
|
|||
color="primary-button"
|
||||
class="white--text ml-0 mt-2 action-upload"
|
||||
depressed
|
||||
@click.stop="uploadDialog()"
|
||||
@click.stop="onUploadDialog()"
|
||||
>
|
||||
<translate key="Upload">Upload</translate>
|
||||
<v-icon :right="!rtl" :left="rtl" dark>cloud_upload</v-icon>
|
||||
|
@ -146,7 +146,7 @@ export default {
|
|||
},
|
||||
cancel() {
|
||||
if (this.busy) {
|
||||
Notify.info(this.$gettext("Uploading photos…"));
|
||||
Notify.info(this.$gettext("Uploading…"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -154,7 +154,7 @@ export default {
|
|||
},
|
||||
confirm() {
|
||||
if (this.busy) {
|
||||
Notify.info(this.$gettext("Uploading photos…"));
|
||||
Notify.info(this.$gettext("Uploading…"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -163,9 +163,6 @@ export default {
|
|||
submit() {
|
||||
// DO NOTHING
|
||||
},
|
||||
uploadDialog() {
|
||||
this.$refs.upload.click();
|
||||
},
|
||||
reset() {
|
||||
this.busy = false;
|
||||
this.selected = [];
|
||||
|
@ -177,7 +174,10 @@ export default {
|
|||
this.completed = 0;
|
||||
this.token = "";
|
||||
},
|
||||
upload() {
|
||||
onUploadDialog() {
|
||||
this.$refs.upload.click();
|
||||
},
|
||||
onUpload() {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
|
@ -199,7 +199,9 @@ export default {
|
|||
this.completed = 0;
|
||||
this.uploads = [];
|
||||
|
||||
Notify.info(this.$gettext("Uploading photos…"));
|
||||
let userUid = this.$session.getUserUID();
|
||||
|
||||
Notify.info(this.$gettext("Uploading…"));
|
||||
|
||||
let addToAlbums = [];
|
||||
|
||||
|
@ -222,7 +224,7 @@ export default {
|
|||
|
||||
formData.append('files', file);
|
||||
|
||||
await Api.post('upload/' + ctx.token,
|
||||
await Api.post(`users/${userUid}/upload/${ctx.token}`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
|
@ -240,9 +242,7 @@ export default {
|
|||
performUpload(this).then(() => {
|
||||
this.indexing = true;
|
||||
const ctx = this;
|
||||
|
||||
Api.post('import/upload/' + this.token, {
|
||||
move: true,
|
||||
Api.post(`users/${userUid}/upload/${ctx.token}`,{
|
||||
albums: addToAlbums,
|
||||
}).then(() => {
|
||||
ctx.reset();
|
||||
|
@ -250,7 +250,7 @@ export default {
|
|||
ctx.$emit('confirm');
|
||||
}).catch(() => {
|
||||
ctx.reset();
|
||||
Notify.error(ctx.$gettext("Failure while importing uploaded files"));
|
||||
Notify.error(ctx.$gettext("Upload failed"));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
|
@ -80,6 +80,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
user: this.$session.getUser(),
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
@ -111,14 +112,29 @@ export default {
|
|||
}
|
||||
},
|
||||
webdavUrl() {
|
||||
return `${window.location.protocol}//admin@${window.location.host}/originals/`;
|
||||
let baseUrl = `${window.location.protocol}//${this.user.Name}@${window.location.host}/originals/`;
|
||||
|
||||
if (this.user.BasePath) {
|
||||
baseUrl = `${baseUrl}${this.user.BasePath}/`;
|
||||
}
|
||||
|
||||
return baseUrl;
|
||||
},
|
||||
windowsUrl() {
|
||||
let baseUrl = "";
|
||||
|
||||
if (window.location.protocol === "https") {
|
||||
return `\\\\${window.location.host}@SSL\\originals\\`;
|
||||
baseUrl = `\\\\${window.location.host}@SSL\\originals\\`;
|
||||
} else {
|
||||
return `\\\\${window.location.host}\\originals\\`;
|
||||
baseUrl = `\\\\${window.location.host}\\originals\\`;
|
||||
}
|
||||
|
||||
if (this.user.BasePath) {
|
||||
const basePath = this.user.BasePath.replace(/\//g, '\\');
|
||||
baseUrl = `${baseUrl}${basePath}\\`;
|
||||
}
|
||||
|
||||
return baseUrl;
|
||||
},
|
||||
windowsHelp(ev) {
|
||||
window.open('https://docs.photoprism.app/user-guide/sync/webdav/#connect-to-a-webdav-server', '_blank');
|
||||
|
|
|
@ -71,7 +71,19 @@ msgstr "Abyss"
|
|||
#: src/component/navigation.vue:73 src/dialog/share/upload.vue:112
|
||||
#: src/model/account.js:98 src/pages/settings.vue:74
|
||||
msgid "Account"
|
||||
msgstr "Profil"
|
||||
msgstr "Konto"
|
||||
|
||||
msgid "Services"
|
||||
msgstr "Dienste"
|
||||
|
||||
msgid "Change Password"
|
||||
msgstr "Passwort ändern"
|
||||
|
||||
msgid "Add Account"
|
||||
msgstr "Konto hinzufügen"
|
||||
|
||||
msgid "Please note that changing your password will log you out on other devices and browsers."
|
||||
msgstr "Bitte beachte, dass du beim Ändern deines Passworts auf anderen Geräten und Browsern abgemeldet wirst."
|
||||
|
||||
#: src/dialog/photo/info.vue:159
|
||||
msgid "Accuracy"
|
||||
|
@ -306,7 +318,7 @@ msgstr "Diese Kategorien wirklich löschen?"
|
|||
|
||||
#: src/dialog/account/remove.vue:10
|
||||
msgid "Are you sure you want to delete this account?"
|
||||
msgstr "Diesen Account wirklich löschen?"
|
||||
msgstr "Dieses Konto wirklich löschen?"
|
||||
|
||||
#: src/dialog/photo/delete.vue:10
|
||||
msgid "Are you sure you want to permanently delete these pictures?"
|
||||
|
@ -822,7 +834,7 @@ msgstr "%{name} bearbeiten"
|
|||
|
||||
#: src/dialog/account/edit.vue:140
|
||||
msgid "Edit Account"
|
||||
msgstr "Account bearbeiten"
|
||||
msgstr "Konto bearbeiten"
|
||||
|
||||
#: src/dialog/photo/edit.vue:51
|
||||
msgid "Edit Photo"
|
||||
|
@ -1906,7 +1918,7 @@ msgstr "Wird neu geladen…"
|
|||
|
||||
#: src/dialog/account/edit.vue:96
|
||||
msgid "Remote Sync"
|
||||
msgstr "Fern-Synchronisation"
|
||||
msgstr "Synchronisation"
|
||||
|
||||
#: src/dialog/photo/people.vue:108
|
||||
msgid "Remove"
|
||||
|
@ -2029,7 +2041,7 @@ msgstr "Server"
|
|||
#: src/dialog/account/add.vue:65 src/dialog/account/edit.vue:420
|
||||
#: src/dialog/share.vue:22
|
||||
msgid "Service URL"
|
||||
msgstr "Service URL"
|
||||
msgstr "Dienst-URL"
|
||||
|
||||
#: src/app/routes.js:326 src/app/routes.js:338 src/app/routes.js:350
|
||||
#: src/app/routes.js:362 src/app/routes.js:374 src/component/navigation.vue:407
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -28,6 +28,7 @@ import Form from "common/form";
|
|||
import Util from "common/util";
|
||||
import Api from "common/api";
|
||||
import { T, $gettext } from "common/vm";
|
||||
import { config } from "app/session";
|
||||
|
||||
export class User extends RestModel {
|
||||
getDefaults() {
|
||||
|
@ -66,33 +67,35 @@ export class User extends RestModel {
|
|||
UpdatedAt: "",
|
||||
},
|
||||
Details: {
|
||||
IdURL: "",
|
||||
SubjUID: "",
|
||||
SubjSrc: "",
|
||||
PlaceID: "",
|
||||
PlaceSrc: "",
|
||||
CellID: "",
|
||||
IdURL: "",
|
||||
AvatarURL: "",
|
||||
SiteURL: "",
|
||||
FeedURL: "",
|
||||
UserGender: "",
|
||||
BirthYear: -1,
|
||||
BirthMonth: -1,
|
||||
BirthDay: -1,
|
||||
NamePrefix: "",
|
||||
GivenName: "",
|
||||
MiddleName: "",
|
||||
FamilyName: "",
|
||||
NameSuffix: "",
|
||||
NickName: "",
|
||||
UserPhone: "",
|
||||
UserAddress: "",
|
||||
UserCountry: "",
|
||||
UserBio: "",
|
||||
JobTitle: "",
|
||||
Department: "",
|
||||
Company: "",
|
||||
CompanyURL: "",
|
||||
BirthYear: -1,
|
||||
BirthMonth: -1,
|
||||
BirthDay: -1,
|
||||
Gender: "",
|
||||
Bio: "",
|
||||
Location: "",
|
||||
Country: "",
|
||||
Phone: "",
|
||||
SiteURL: "",
|
||||
ProfileURL: "",
|
||||
FeedURL: "",
|
||||
AvatarURL: "",
|
||||
OrgName: "",
|
||||
OrgTitle: "",
|
||||
OrgEmail: "",
|
||||
OrgPhone: "",
|
||||
OrgURL: "",
|
||||
CreatedAt: "",
|
||||
UpdatedAt: "",
|
||||
},
|
||||
|
@ -149,6 +152,36 @@ export class User extends RestModel {
|
|||
);
|
||||
}
|
||||
|
||||
getAvatarURL(size) {
|
||||
if (!this.Thumb) {
|
||||
return `${config.contentUri}/svg/user`;
|
||||
}
|
||||
|
||||
if (!size) {
|
||||
size = "tile_500";
|
||||
}
|
||||
|
||||
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken}/${size}`;
|
||||
}
|
||||
|
||||
uploadAvatar(files) {
|
||||
if (this.busy) {
|
||||
return;
|
||||
} else if (!files || files.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let file = files[0];
|
||||
let formData = new FormData();
|
||||
let formConf = { headers: { "Content-Type": "multipart/form-data" } };
|
||||
|
||||
formData.append("files", file);
|
||||
|
||||
return Api.post(this.getEntityResource() + `/avatar`, formData, formConf).then((response) =>
|
||||
Promise.resolve(this.setValues(response.data))
|
||||
);
|
||||
}
|
||||
|
||||
getProfileForm() {
|
||||
return Api.options(this.getEntityResource() + "/profile").then((response) =>
|
||||
Promise.resolve(new Form(response.data))
|
||||
|
@ -162,8 +195,8 @@ export class User extends RestModel {
|
|||
}).then((response) => Promise.resolve(response.data));
|
||||
}
|
||||
|
||||
saveProfile() {
|
||||
return Api.post(this.getEntityResource() + "/profile", this.getValues()).then((response) =>
|
||||
save() {
|
||||
return Api.post(this.getEntityResource(), this.getValues()).then((response) =>
|
||||
Promise.resolve(this.setValues(response.data))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -46,8 +46,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import tabColors from "pages/discover/colors.vue";
|
||||
import tabTodo from "pages/discover/todo.vue";
|
||||
import tabColors from "page/discover/colors.vue";
|
||||
import tabTodo from "page/discover/todo.vue";
|
||||
|
||||
export default {
|
||||
name: 'PPageSettings',
|
|
@ -8,7 +8,7 @@
|
|||
slider-color="secondary-dark"
|
||||
:height="$vuetify.breakpoint.smAndDown ? 48 : 64"
|
||||
>
|
||||
<v-tab v-for="(item, index) in tabs" :id="'tab-' + item.name" :key="manage" :class="item.class" ripple
|
||||
<v-tab v-for="(item, index) in tabs" :id="'tab-' + item.name" :key="index" :class="item.class" ripple
|
||||
@click="changePath(item.path)">
|
||||
<v-icon v-if="$vuetify.breakpoint.smAndDown" :title="item.label">{{ item.icon }}</v-icon>
|
||||
<template v-else>
|
||||
|
@ -17,7 +17,7 @@
|
|||
</v-tab>
|
||||
|
||||
<v-tabs-items touchless>
|
||||
<v-tab-item v-for="(item, index) in tabs" :key="manage" lazy>
|
||||
<v-tab-item v-for="(item, index) in tabs" :key="index" lazy>
|
||||
<component :is="item.component"></component>
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
|
@ -26,9 +26,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Import from "pages/files/import.vue";
|
||||
import Index from "pages/files/index.vue";
|
||||
import Logs from "pages/files/logs.vue";
|
||||
import Import from "page/library/import.vue";
|
||||
import Index from "page/library/index.vue";
|
||||
import Logs from "page/library/logs.vue";
|
||||
|
||||
function initTabs(flag, tabs) {
|
||||
let i = 0;
|
||||
|
@ -58,7 +58,7 @@ export default {
|
|||
|
||||
const tabs = [
|
||||
{
|
||||
'name': 'index',
|
||||
'name': 'library_index',
|
||||
'component': Index,
|
||||
'label': this.$gettext('Index'),
|
||||
'class': '',
|
||||
|
@ -68,7 +68,7 @@ export default {
|
|||
'demo': true,
|
||||
},
|
||||
{
|
||||
'name': 'import',
|
||||
'name': 'library_import',
|
||||
'component': Import,
|
||||
'label': this.$gettext('Import'),
|
||||
'class': '',
|
||||
|
@ -81,12 +81,12 @@ export default {
|
|||
|
||||
if(this.$config.feature('logs')) {
|
||||
tabs.push({
|
||||
'name': 'logs',
|
||||
'name': 'library_logs',
|
||||
'component': Logs,
|
||||
'label': this.$gettext('Logs'),
|
||||
'class': '',
|
||||
'path': '/logs',
|
||||
'icon': 'grading',
|
||||
'icon': 'assignment',
|
||||
'readonly': true,
|
||||
'demo': true,
|
||||
});
|
||||
|
@ -102,7 +102,9 @@ export default {
|
|||
|
||||
let active = 0;
|
||||
|
||||
if (typeof this.tab === 'string' && this.tab !== '') {
|
||||
if (typeof this.$route.name === 'string' && this.$route.name !== '') {
|
||||
active = tabs.findIndex((t) => t.name === this.$route.name);
|
||||
} else if (typeof this.tab === 'string' && this.tab !== '') {
|
||||
active = tabs.findIndex((t) => t.name === this.tab);
|
||||
}
|
||||
|
||||
|
@ -116,6 +118,24 @@ export default {
|
|||
rtl: this.$rtl,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
'$route'() {
|
||||
let active = this.active;
|
||||
|
||||
if (typeof this.$route.name === 'string' && this.$route.name !== '') {
|
||||
active = this.tabs.findIndex((t) => t.name === this.$route.name);
|
||||
}
|
||||
|
||||
if (active >= 0) {
|
||||
this.active = active;
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (!this.tabs || this.tabs.length === 0) {
|
||||
this.$router.push({ name: "albums" });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changePath: function (path) {
|
||||
if (this.$route.path !== path) {
|
|
@ -4,6 +4,7 @@
|
|||
v-model="active"
|
||||
flat
|
||||
grow
|
||||
touchless
|
||||
color="secondary"
|
||||
slider-color="secondary-dark"
|
||||
:height="$vuetify.breakpoint.smAndDown ? 48 : 64"
|
||||
|
@ -33,8 +34,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Recognized from "pages/people/recognized.vue";
|
||||
import NewFaces from "pages/people/new.vue";
|
||||
import Recognized from "page/people/recognized.vue";
|
||||
import NewFaces from "page/people/new.vue";
|
||||
|
||||
export default {
|
||||
name: 'PPagePeople',
|
|
@ -112,7 +112,7 @@
|
|||
</v-layout>
|
||||
<div class="text-xs-center mt-3 mb-2">
|
||||
<v-btn
|
||||
color="secondary" round
|
||||
color="secondary" round depressed
|
||||
:to="{name: 'all', query: { q: 'face:new' }}"
|
||||
>
|
||||
<translate>Show all new faces</translate>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div :class="$config.aclClasses('settings')" class="p-page p-page-settings">
|
||||
<div class="p-page p-page-settings" :class="$config.aclClasses('settings')">
|
||||
<v-tabs
|
||||
v-model="active"
|
||||
flat
|
||||
|
@ -28,11 +28,11 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import General from "pages/settings/general.vue";
|
||||
import Files from "pages/settings/files.vue";
|
||||
import Advanced from "pages/settings/advanced.vue";
|
||||
import Services from "pages/settings/services.vue";
|
||||
import Account from "pages/settings/account.vue";
|
||||
import General from "page/settings/general.vue";
|
||||
import Library from "page/settings/library.vue";
|
||||
import Advanced from "page/settings/advanced.vue";
|
||||
import Services from "page/settings/services.vue";
|
||||
import Account from "page/settings/account.vue";
|
||||
import {config} from "app/session";
|
||||
|
||||
function initTabs(flag, tabs) {
|
||||
|
@ -60,7 +60,7 @@ export default {
|
|||
|
||||
const tabs = [
|
||||
{
|
||||
'name': 'settings-general',
|
||||
'name': 'settings_general',
|
||||
'component': General,
|
||||
'label': this.$gettext('General'),
|
||||
'class': '',
|
||||
|
@ -72,11 +72,11 @@ export default {
|
|||
'show': config.feature('settings'),
|
||||
},
|
||||
{
|
||||
'name': 'settings-files',
|
||||
'component': Files,
|
||||
'name': 'settings_media',
|
||||
'component': Library,
|
||||
'label': this.$gettext('Library'),
|
||||
'class': '',
|
||||
'path': '/settings/files',
|
||||
'path': '/settings/media',
|
||||
'icon': 'camera_roll',
|
||||
'public': true,
|
||||
'admin': true,
|
||||
|
@ -84,7 +84,7 @@ export default {
|
|||
'show': config.allow("config", "manage"),
|
||||
},
|
||||
{
|
||||
'name': 'settings-advanced',
|
||||
'name': 'settings_advanced',
|
||||
'component': Advanced,
|
||||
'label': this.$gettext('Advanced'),
|
||||
'class': '',
|
||||
|
@ -96,7 +96,7 @@ export default {
|
|||
'show': config.allow("config", "manage"),
|
||||
},
|
||||
{
|
||||
'name': 'settings-services',
|
||||
'name': 'settings_services',
|
||||
'component': Services,
|
||||
'label': this.$gettext('Services'),
|
||||
'class': '',
|
||||
|
@ -108,7 +108,7 @@ export default {
|
|||
'show': config.feature('services') && config.allow("services", "manage"),
|
||||
},
|
||||
{
|
||||
'name': 'settings-account',
|
||||
'name': 'settings_account',
|
||||
'component': Account,
|
||||
'label': this.$gettext('Account'),
|
||||
'class': '',
|
||||
|
@ -131,7 +131,9 @@ export default {
|
|||
|
||||
let active = 0;
|
||||
|
||||
if (typeof this.tab === 'string' && this.tab !== '') {
|
||||
if (typeof this.$route.name === 'string' && this.$route.name !== '') {
|
||||
active = tabs.findIndex((t) => t.name === this.$route.name);
|
||||
} else if (typeof this.tab === 'string' && this.tab !== '') {
|
||||
active = tabs.findIndex((t) => t.name === this.tab);
|
||||
}
|
||||
|
||||
|
@ -144,6 +146,24 @@ export default {
|
|||
rtl: this.$rtl,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
'$route'() {
|
||||
let active = this.active;
|
||||
|
||||
if (typeof this.$route.name === 'string' && this.$route.name !== '') {
|
||||
active = this.tabs.findIndex((t) => t.name === this.$route.name);
|
||||
}
|
||||
|
||||
if (active >= 0) {
|
||||
this.active = active;
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (!this.tabs || this.tabs.length === 0) {
|
||||
this.$router.push({ name: "albums" });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changePath: function (path) {
|
||||
if (this.$route.path !== path) {
|
352
frontend/src/page/settings/account.vue
Normal file
352
frontend/src/page/settings/account.vue
Normal file
|
@ -0,0 +1,352 @@
|
|||
<template>
|
||||
<div class="p-tab p-settings-account">
|
||||
<v-form ref="form" lazy-validation
|
||||
dense class="p-form-account" accept-charset="UTF-8"
|
||||
@submit.prevent="onChange">
|
||||
<input ref="upload" type="file" class="d-none input-upload" @change.stop="onUploadAvatar()">
|
||||
<v-card flat tile class="mt-2 px-1 application">
|
||||
<v-card-actions>
|
||||
<v-layout row wrap align-top>
|
||||
<v-flex
|
||||
class="p-photo pa-3 text-xs-center"
|
||||
xs4 sm3 md2 xl1 fill-height
|
||||
>
|
||||
<div class="user-avatar" @click.exact="onChangeAvatar()">
|
||||
<v-img :src="user.getAvatarURL()"
|
||||
:alt="displayName" aspect-ratio="1"
|
||||
class="grey lighten-1 elevation-0 clickable"
|
||||
></v-img>
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex xs8 sm9 md10 xl11 fill-height class="pa-0">
|
||||
<v-layout wrap align-top>
|
||||
<v-flex xs12 md3 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="user.Name"
|
||||
hide-details required box flat readonly
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Username')"
|
||||
class="input-name"
|
||||
color="secondary-dark"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12 md9 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="user.DisplayName"
|
||||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Display Name')"
|
||||
class="input-display-name"
|
||||
color="secondary-dark"
|
||||
@change="onChange"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="user.Email"
|
||||
hide-details required box flat
|
||||
type="email"
|
||||
:disabled="busy"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Email')"
|
||||
class="input-email"
|
||||
color="secondary-dark"
|
||||
@change="onChange"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-card flat tile class="mt-0 px-1 application">
|
||||
<v-card-title primary-title class="pb-1">
|
||||
<h3 class="body-2 mb-0">
|
||||
<translate>Security and Access</translate>
|
||||
</h3>
|
||||
</v-card-title>
|
||||
<v-card-actions>
|
||||
<v-layout wrap align-top>
|
||||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-btn block depressed color="secondary-light" class="action-change-password compact" :disabled="isPublic || isDemo || user.Name === ''"
|
||||
@click.stop="showDialog('password')">
|
||||
<translate>Change Password</translate>
|
||||
<v-icon :right="!rtl" :left="rtl" dark>lock</v-icon>
|
||||
</v-btn>
|
||||
</v-flex>
|
||||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-btn block depressed color="secondary-light" class="action-webdav-dialog compact"
|
||||
:disabled="isPublic || isDemo || !user.WebDAV" @click.stop="showDialog('webdav')">
|
||||
<translate>Connect via WebDAV</translate>
|
||||
<v-icon :right="!rtl" :left="rtl" dark>sync_alt</v-icon>
|
||||
</v-btn>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-card flat tile class="mt-0 px-1 application">
|
||||
<v-card-title primary-title class="pb-1">
|
||||
<h3 class="body-2 mb-0">
|
||||
<translate>Work Details</translate>
|
||||
</h3>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-actions>
|
||||
<v-layout wrap align-top>
|
||||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="user.Details.OrgName"
|
||||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Organization')"
|
||||
class="input-org-name"
|
||||
color="secondary-dark"
|
||||
@change="onChange"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs6 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="user.Details.OrgURL"
|
||||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
type="url"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('URL')"
|
||||
class="input-org-url"
|
||||
color="secondary-dark"
|
||||
@change="onChange"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs6 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="user.Details.OrgTitle"
|
||||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Title')"
|
||||
class="input-position"
|
||||
color="secondary-dark"
|
||||
@change="onChange"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="user.Details.OrgEmail"
|
||||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
type="email"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Email')"
|
||||
class="input-org-email"
|
||||
color="secondary-dark"
|
||||
@change="onChange"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-card flat tile class="mt-0 px-1 application">
|
||||
<v-card-title primary-title class="pb-1">
|
||||
<h3 class="body-2 mb-0">
|
||||
<translate>Contact Details</translate>
|
||||
</h3>
|
||||
</v-card-title>
|
||||
<v-card-actions>
|
||||
<v-layout wrap align-top>
|
||||
<v-flex xs12 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="user.Details.Location"
|
||||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Location')"
|
||||
class="input-location"
|
||||
color="secondary-dark"
|
||||
@change="onChange"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-autocomplete
|
||||
v-model="user.Details.Country"
|
||||
:disabled="busy"
|
||||
:label="$gettext('Country')" hide-details box flat
|
||||
hide-no-data
|
||||
browser-autocomplete="off"
|
||||
color="secondary-dark"
|
||||
item-value="Code"
|
||||
item-text="Name"
|
||||
:items="countries"
|
||||
class="input-country"
|
||||
@change="onChange"
|
||||
>
|
||||
</v-autocomplete>
|
||||
</v-flex>
|
||||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="user.Details.Phone"
|
||||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Phone')"
|
||||
class="input-phone"
|
||||
color="secondary-dark"
|
||||
@change="onChange"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="user.Details.ProfileURL"
|
||||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
type="url"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Profile')"
|
||||
class="input-profile-url"
|
||||
color="secondary-dark"
|
||||
@change="onChange"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="user.Details.FeedURL"
|
||||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
type="url"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Feed')"
|
||||
class="input-feed-url"
|
||||
color="secondary-dark"
|
||||
@change="onChange"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12 class="pa-2">
|
||||
<v-textarea v-model="user.Details.Bio" auto-grow flat box hide-details
|
||||
rows="3" class="input-bio" color="secondary-dark"
|
||||
autocorrect="off" autocapitalize="none" browser-autocomplete="off"
|
||||
:disabled="busy"
|
||||
:label="$gettext('Bio')"
|
||||
@change="onChange"></v-textarea>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-form>
|
||||
<p-about-footer></p-about-footer>
|
||||
<p-account-password-dialog :show="dialog.password" @cancel="dialog.password = false" @confirm="dialog.password = false"></p-account-password-dialog>
|
||||
<p-webdav-dialog :show="dialog.webdav" @close="dialog.webdav = false"></p-webdav-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PAccountPasswordDialog from "dialog/account/password.vue";
|
||||
import countries from "options/countries.json";
|
||||
import Notify from "common/notify";
|
||||
import Api from "common/api";
|
||||
import User from "model/user";
|
||||
|
||||
export default {
|
||||
name: 'PSettingsAccount',
|
||||
components: {PAccountPasswordDialog},
|
||||
data() {
|
||||
const isDemo = this.$config.isDemo();
|
||||
const isPublic = this.$config.isPublic();
|
||||
return {
|
||||
busy: isDemo || isPublic,
|
||||
isDemo: isDemo,
|
||||
isPublic: isPublic,
|
||||
rtl: this.$rtl,
|
||||
user: new User(this.$session.getUser()),
|
||||
countries: countries,
|
||||
dialog: {
|
||||
password: false,
|
||||
webdav: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if(this.isPublic && !this.isDemo) {
|
||||
this.$router.push({ name: "settings" });
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayName() {
|
||||
const user = this.$session.getUser();
|
||||
if (user) {
|
||||
return user.getDisplayName();
|
||||
}
|
||||
|
||||
return this.$gettext("Unregistered");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showDialog(name) {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
this.dialog[name] = true;
|
||||
},
|
||||
disabled() {
|
||||
return (this.isDemo || this.busy);
|
||||
},
|
||||
onChangeAvatar() {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
this.$refs.upload.click();
|
||||
},
|
||||
onLogout() {
|
||||
this.$session.logout();
|
||||
},
|
||||
onChange() {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
this.busy = true;
|
||||
this.user.update().then((u) => {
|
||||
this.user = new User(u);
|
||||
this.$session.setUser(u);
|
||||
this.$notify.success(this.$gettext("Settings saved"));
|
||||
}).finally(() => this.busy = false);
|
||||
},
|
||||
onUploadAvatar() {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.busy = true;
|
||||
|
||||
Notify.info(this.$gettext("Uploading…"));
|
||||
|
||||
this.user.uploadAvatar(this.$refs.upload.files).then((u) => {
|
||||
this.user = new User(u);
|
||||
this.$session.setUser(u);
|
||||
this.$notify.success(this.$gettext("Settings saved"));
|
||||
}).finally(() => this.busy = false);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="p-tab p-settings-general">
|
||||
<div class="p-tab p-settings-advanced">
|
||||
<v-form ref="form" lazy-validation
|
||||
dense class="p-form-settings" accept-charset="UTF-8"
|
||||
@submit.prevent="onChange">
|
||||
|
@ -332,7 +332,7 @@ import ConfigOptions from "model/config-options";
|
|||
import * as options from "options/options";
|
||||
|
||||
export default {
|
||||
name: 'PSettingsServer',
|
||||
name: 'PSettingsAdvanced',
|
||||
data() {
|
||||
return {
|
||||
busy: this.$config.get("demo"),
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="p-tab p-settings-general">
|
||||
<div class="p-tab p-settings-library">
|
||||
<v-form ref="form" lazy-validation
|
||||
dense class="p-form-settings" accept-charset="UTF-8"
|
||||
@submit.prevent="onChange">
|
||||
|
@ -160,7 +160,7 @@ export default {
|
|||
this.$config.load().then(() => {
|
||||
this.settings.setValues(this.$config.settings());
|
||||
this.busy = false;
|
||||
})
|
||||
});
|
||||
},
|
||||
onChange() {
|
||||
const reload = this.settings.changed("ui", "language");
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="p-tab p-settings-sync">
|
||||
<div class="p-tab p-settings-services">
|
||||
<v-data-table
|
||||
v-model="selected"
|
||||
:headers="listColumns"
|
||||
|
@ -8,7 +8,7 @@
|
|||
disable-initial-sort
|
||||
class="elevation-0 p-accounts p-accounts-list p-results"
|
||||
item-key="ID"
|
||||
:no-data-text="$gettext('No servers configured.')"
|
||||
:no-data-text="$gettext('No services configured.')"
|
||||
>
|
||||
<template #items="props">
|
||||
<td class="p-account">
|
||||
|
@ -64,27 +64,28 @@
|
|||
dense class="p-form-settings mt-2" accept-charset="UTF-8"
|
||||
@submit.prevent="add">
|
||||
|
||||
<v-btn depressed color="secondary-light" class="action-webdav-dialog compact ml-0 my-2 mr-2"
|
||||
<v-btn v-if="user.WebDAV" depressed color="secondary-light" class="action-webdav-dialog compact ml-0 my-2 mr-2"
|
||||
:disabled="isPublic || isDemo" @click.stop="webdavDialog">
|
||||
<translate>Connect via WebDAV</translate>
|
||||
<v-icon :right="rtl" :left="!rtl" dark>sync_alt</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn color="primary-button"
|
||||
class="white--text compact ml-0 my-2 mr-2"
|
||||
:disabled="isPublic || isDemo"
|
||||
depressed @click.stop="add">
|
||||
<translate>Add Server</translate>
|
||||
<translate>Connect</translate>
|
||||
<v-icon :right="!rtl" :left="rtl" dark>add</v-icon>
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</v-container>
|
||||
<p-account-add-dialog :show="dialog.add" @cancel="onCancel('add')"
|
||||
@confirm="onAdded"></p-account-add-dialog>
|
||||
<p-account-remove-dialog :show="dialog.remove" :model="model" @cancel="onCancel('remove')"
|
||||
@confirm="onRemoved"></p-account-remove-dialog>
|
||||
<p-account-edit-dialog :show="dialog.edit" :model="model" :scope="editScope" @remove="remove(model)"
|
||||
<p-service-add-dialog :show="dialog.add" @cancel="onCancel('add')"
|
||||
@confirm="onAdded"></p-service-add-dialog>
|
||||
<p-service-remove-dialog :show="dialog.remove" :model="model" @cancel="onCancel('remove')"
|
||||
@confirm="onRemoved"></p-service-remove-dialog>
|
||||
<p-service-edit-dialog :show="dialog.edit" :model="model" :scope="editScope" @remove="remove(model)"
|
||||
@cancel="onCancel('edit')"
|
||||
@confirm="onEdited"></p-account-edit-dialog>
|
||||
@confirm="onEdited"></p-service-edit-dialog>
|
||||
<p-webdav-dialog :show="dialog.webdav" @close="dialog.webdav = false"></p-webdav-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -95,7 +96,7 @@ import Service from "model/service";
|
|||
import {DateTime} from "luxon";
|
||||
|
||||
export default {
|
||||
name: 'PSettingsSync',
|
||||
name: 'PSettingsServices',
|
||||
data() {
|
||||
return {
|
||||
isDemo: this.$config.get("demo"),
|
||||
|
@ -107,6 +108,7 @@ export default {
|
|||
results: [],
|
||||
labels: {},
|
||||
selected: [],
|
||||
user: this.$session.getUser(),
|
||||
dialog: {
|
||||
add: false,
|
||||
remove: false,
|
||||
|
@ -114,7 +116,7 @@ export default {
|
|||
},
|
||||
editScope: "main",
|
||||
listColumns: [
|
||||
{text: this.$gettext('Server'), value: 'AccName', sortable: false, align: 'left'},
|
||||
{text: this.$gettext('Name'), value: 'AccName', sortable: false, align: 'left'},
|
||||
{text: this.$gettext('Upload'), value: 'AccShare', sortable: false, align: 'center'},
|
||||
{text: this.$gettext('Sync'), value: 'AccSync', sortable: false, align: 'center'},
|
||||
{
|
|
@ -1,114 +0,0 @@
|
|||
<template>
|
||||
<div class="p-tab p-settings-account">
|
||||
<v-form ref="form" dense class="form-password" accept-charset="UTF-8">
|
||||
<v-card flat tile class="ma-2 application">
|
||||
<v-card-actions>
|
||||
<v-layout wrap align-top>
|
||||
<v-flex xs12 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="oldPassword"
|
||||
hide-details required
|
||||
type="password"
|
||||
:disabled="busy"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Current Password')"
|
||||
class="input-current-password"
|
||||
color="secondary-dark"
|
||||
placeholder="••••••••"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="newPassword"
|
||||
required counter persistent-hint
|
||||
type="password"
|
||||
:disabled="busy"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('New Password')"
|
||||
class="input-new-password"
|
||||
color="secondary-dark"
|
||||
placeholder="••••••••"
|
||||
:hint="$gettext('Must have at least 8 characters.')"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="confirmPassword"
|
||||
required counter persistent-hint
|
||||
type="password"
|
||||
:disabled="busy"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Retype Password')"
|
||||
class="input-retype-password"
|
||||
color="secondary-dark"
|
||||
placeholder="••••••••"
|
||||
:hint="$gettext('Please confirm your new password.')"
|
||||
@keyup.enter.native="confirm"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 class="pa-2">
|
||||
<p class="caption pa-0">
|
||||
<translate>Note: Updating your password will invalidate other browser sessions, so you will have to log in again.</translate>
|
||||
</p>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 class="pa-2">
|
||||
<v-btn depressed color="primary-button"
|
||||
:disabled="disabled()"
|
||||
class="action-confirm compact white--text ma-0"
|
||||
@click.stop="confirm">
|
||||
<translate>Change</translate>
|
||||
<v-icon :right="!rtl" :left="rtl" dark>keyboard_return</v-icon>
|
||||
</v-btn>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-form>
|
||||
|
||||
<p-about-footer></p-about-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'PSettingsAccount',
|
||||
data() {
|
||||
return {
|
||||
busy: false,
|
||||
isDemo: this.$config.get("demo"),
|
||||
isPublic: this.$config.get("public"),
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
rtl: this.$rtl,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if(this.isPublic && !this.isDemo) {
|
||||
this.$router.push({ name: "settings" });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
disabled() {
|
||||
return (this.isDemo || this.busy || this.oldPassword === "" || this.newPassword.length < 8 || (this.newPassword !== this.confirmPassword));
|
||||
},
|
||||
confirm() {
|
||||
this.busy = true;
|
||||
this.$session.getUser().changePassword(this.oldPassword, this.newPassword).then(() => {
|
||||
this.$notify.success(this.$gettext("Password changed"));
|
||||
}).finally(() => this.busy = false);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -188,14 +188,14 @@ func UpdateService(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// 2) Update form with values from request
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
if err = c.BindJSON(&f); err != nil {
|
||||
log.Error(err)
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 3) Save model with values from form
|
||||
if err := m.SaveForm(f); err != nil {
|
||||
if err = m.SaveForm(f); err != nil {
|
||||
log.Error(err)
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
|
|
|
@ -6,6 +6,15 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var userIconSvg = []byte(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M11.1 35.25q3.15-2.2 6.25-3.375Q20.45 30.7 24 30.7q3.55 0 6.675 1.175t6.275 3.375q2.2-2.7 3.125-5.45Q41 27.05 41 24q0-7.25-4.875-12.125T24 7q-7.25 0-12.125 4.875T7 24q0 3.05.95 5.8t3.15 5.45ZM24 25.5q-2.9 0-4.875-1.975T17.15 18.65q0-2.9 1.975-4.875T24 11.8q2.9 0 4.875 1.975t1.975 4.875q0 2.9-1.975 4.875T24 25.5ZM24 44q-4.1 0-7.75-1.575-3.65-1.575-6.375-4.3-2.725-2.725-4.3-6.375Q4 28.1 4 24q0-4.15 1.575-7.775t4.3-6.35q2.725-2.725 6.375-4.3Q19.9 4 24 4q4.15 0 7.775 1.575t6.35 4.3q2.725 2.725 4.3 6.35Q44 19.85 44 24q0 4.1-1.575 7.75-1.575 3.65-4.3 6.375-2.725 2.725-6.35 4.3Q28.15 44 24 44Zm0-3q2.75 0 5.375-.8t5.175-2.8q-2.55-1.8-5.2-2.75-2.65-.95-5.35-.95-2.7 0-5.35.95-2.65.95-5.2 2.75 2.55 2 5.175 2.8Q21.25 41 24 41Zm0-18.5q1.7 0 2.775-1.075t1.075-2.775q0-1.7-1.075-2.775T24 14.8q-1.7 0-2.775 1.075T20.15 18.65q0 1.7 1.075 2.775T24 22.5Zm0-3.85Zm0 18.7Z"/></svg>`)
|
||||
|
||||
var faceIconSvg = []byte(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M31.3 21.35q1.15 0 1.925-.8.775-.8.775-1.9 0-1.15-.775-1.925-.775-.775-1.925-.775-1.1 0-1.9.775-.8.775-.8 1.925 0 1.1.8 1.9.8.8 1.9.8Zm-14.6 0q1.15 0 1.925-.8.775-.8.775-1.9 0-1.15-.775-1.925-.775-.775-1.925-.775-1.1 0-1.9.775-.8.775-.8 1.925 0 1.1.8 1.9.8.8 1.9.8Zm7.3 13.6q3.3 0 6.075-1.775Q32.85 31.4 34.1 28.35h-2.6q-1.15 2-3.15 3.075-2 1.075-4.3 1.075-2.35 0-4.375-1.05t-3.125-3.1H13.9q1.3 3.05 4.05 4.825Q20.7 34.95 24 34.95ZM24 44q-4.15 0-7.8-1.575-3.65-1.575-6.35-4.275-2.7-2.7-4.275-6.35Q4 28.15 4 24t1.575-7.8Q7.15 12.55 9.85 9.85q2.7-2.7 6.35-4.275Q19.85 4 24 4t7.8 1.575q3.65 1.575 6.35 4.275 2.7 2.7 4.275 6.35Q44 19.85 44 24t-1.575 7.8q-1.575 3.65-4.275 6.35-2.7 2.7-6.35 4.275Q28.15 44 24 44Zm0-20Zm0 17q7.1 0 12.05-4.95Q41 31.1 41 24q0-7.1-4.95-12.05Q31.1 7 24 7q-7.1 0-12.05 4.95Q7 16.9 7 24q0 7.1 4.95 12.05Q16.9 41 24 41Z"/></svg>`)
|
||||
|
||||
var cameraIconSvg = []byte(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M24 34.7q3.6 0 6.05-2.45 2.45-2.45 2.45-6.05 0-3.65-2.45-6.075Q27.6 17.7 24 17.7q-3.65 0-6.075 2.425Q15.5 22.55 15.5 26.2q0 3.6 2.425 6.05Q20.35 34.7 24 34.7ZM7 42q-1.2 0-2.1-.9Q4 40.2 4 39V13.35q0-1.15.9-2.075.9-.925 2.1-.925h7.35L18 6h12l3.65 4.35H41q1.15 0 2.075.925Q44 12.2 44 13.35V39q0 1.2-.925 2.1-.925.9-2.075.9Z"/></svg>`)
|
||||
|
||||
var photoIconSvg = []byte(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/>
|
||||
<path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-4.86 8.86l-3 3.87L9 13.14 6 17h12l-3.86-5.14z"/></svg>`)
|
||||
|
@ -41,8 +50,22 @@ var uncachedIconSvg = []byte(`
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>`)
|
||||
|
||||
// GetSvg returns SVG placeholder symbols.
|
||||
//
|
||||
// GET /api/v1/svg/*
|
||||
func GetSvg(router *gin.RouterGroup) {
|
||||
router.GET("/svg/user", func(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "image/svg+xml", userIconSvg)
|
||||
})
|
||||
|
||||
router.GET("/svg/face", func(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "image/svg+xml", faceIconSvg)
|
||||
})
|
||||
|
||||
router.GET("/svg/camera", func(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "image/svg+xml", cameraIconSvg)
|
||||
})
|
||||
|
||||
router.GET("/svg/photo", func(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
|
||||
})
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
||||
// Upload adds files to the import folder, from where supported file types are moved to the originals folders.
|
||||
//
|
||||
// POST /api/v1/upload/:path
|
||||
func Upload(router *gin.RouterGroup) {
|
||||
router.POST("/upload/:token", func(c *gin.Context) {
|
||||
conf := get.Config()
|
||||
|
||||
if conf.ReadOnly() || !conf.Settings().Features.Upload {
|
||||
Abort(c, http.StatusForbidden, i18n.ErrReadOnly)
|
||||
return
|
||||
}
|
||||
|
||||
s := AuthAny(c, acl.ResourceFiles, acl.Permissions{acl.ActionManage, acl.ActionUpload})
|
||||
|
||||
if s.Abort(c) {
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
token := clean.Token(c.Param("token"))
|
||||
|
||||
f, err := c.MultipartForm()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("upload: %s", err)
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
event.Publish("upload.start", event.Data{"time": start})
|
||||
|
||||
files := f.File["files"]
|
||||
uploaded := len(files)
|
||||
|
||||
var uploads []string
|
||||
|
||||
uploadDir := path.Join(conf.ImportPath(), UploadPath, s.RefID+token)
|
||||
|
||||
if err = os.MkdirAll(uploadDir, os.ModePerm); err != nil {
|
||||
log.Errorf("upload: failed creating folder %s", clean.Log(filepath.Base(uploadDir)))
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
filename := path.Join(uploadDir, filepath.Base(file.Filename))
|
||||
|
||||
log.Debugf("upload: saving file %s", clean.Log(file.Filename))
|
||||
|
||||
if err := c.SaveUploadedFile(file, filename); err != nil {
|
||||
log.Errorf("upload: failed saving file %s", clean.Log(filepath.Base(file.Filename)))
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
uploads = append(uploads, filename)
|
||||
}
|
||||
|
||||
if !conf.UploadNSFW() {
|
||||
nd := get.NsfwDetector()
|
||||
|
||||
containsNSFW := false
|
||||
|
||||
for _, filename := range uploads {
|
||||
labels, err := nd.File(filename)
|
||||
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if labels.IsSafe() {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("nsfw: %s might be offensive", clean.Log(filename))
|
||||
|
||||
containsNSFW = true
|
||||
}
|
||||
|
||||
if containsNSFW {
|
||||
for _, filename := range uploads {
|
||||
if err := os.Remove(filename); err != nil {
|
||||
log.Errorf("nsfw: could not delete %s", clean.Log(filename))
|
||||
}
|
||||
}
|
||||
|
||||
Abort(c, http.StatusForbidden, i18n.ErrOffensiveUpload)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := int(time.Since(start).Seconds())
|
||||
|
||||
msg := i18n.Msg(i18n.MsgFilesUploadedIn, uploaded, elapsed)
|
||||
|
||||
log.Info(msg)
|
||||
|
||||
c.JSON(http.StatusOK, i18n.Response{Code: http.StatusOK, Msg: msg})
|
||||
})
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUpload(t *testing.T) {
|
||||
t.Run("forbidden", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
Upload(router)
|
||||
r := PerformRequest(app, "POST", "/api/v1/upload/xxx")
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
}
|
119
internal/api/users_avatar.go
Normal file
119
internal/api/users_avatar.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
||||
// UploadUserAvatar updates the avatar image of the currently authenticated user.
|
||||
//
|
||||
// POST /api/v1/users/:uid/avatar
|
||||
func UploadUserAvatar(router *gin.RouterGroup) {
|
||||
router.POST("/users/:uid/avatar", func(c *gin.Context) {
|
||||
conf := get.Config()
|
||||
|
||||
if conf.Demo() || conf.DisableSettings() {
|
||||
AbortForbidden(c)
|
||||
return
|
||||
}
|
||||
|
||||
s := AuthAny(c, acl.ResourceUsers, acl.Permissions{acl.ActionManage, acl.AccessOwn})
|
||||
|
||||
if s.Abort(c) {
|
||||
return
|
||||
}
|
||||
|
||||
uid := clean.UID(c.Param("uid"))
|
||||
|
||||
// Users may only change their own avatar.
|
||||
if s.User().UserUID != uid {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "user uid does not match"}, s.RefID)
|
||||
AbortForbidden(c)
|
||||
return
|
||||
}
|
||||
|
||||
f, err := c.MultipartForm()
|
||||
|
||||
if err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "%s"}, s.RefID, err)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed)
|
||||
return
|
||||
}
|
||||
|
||||
files := f.File["files"]
|
||||
|
||||
if len(files) != 1 {
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed)
|
||||
return
|
||||
}
|
||||
|
||||
uploadDir, err := conf.UserUploadPath(s.UserUID, "")
|
||||
|
||||
if err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "failed to create folder", "%s"}, s.RefID, err)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed)
|
||||
return
|
||||
}
|
||||
|
||||
file := files[0]
|
||||
|
||||
// Uploaded images must be JPEGs with a maximum file size of 10 MB.
|
||||
if file.Size > 10000000 {
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", "upload avatar", "file size exceeded"}, s.RefID)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrFileTooLarge)
|
||||
return
|
||||
} else if fReader, fErr := file.Open(); fErr != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "%s"}, s.RefID, err)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed)
|
||||
return
|
||||
} else if mimeType, mimeErr := mimetype.DetectReader(fReader); mimeErr != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "%s"}, s.RefID, err)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed)
|
||||
return
|
||||
} else if !mimeType.Is(fs.MimeTypeJpeg) {
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", "upload avatar", "only jpeg supported"}, s.RefID)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrWrongFileType)
|
||||
return
|
||||
}
|
||||
|
||||
fileName := "avatar.jpg"
|
||||
filePath := path.Join(uploadDir, fileName)
|
||||
|
||||
if err = c.SaveUploadedFile(file, filePath); err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "failed to save %s"}, s.RefID, clean.Log(filePath))
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed)
|
||||
return
|
||||
} else {
|
||||
event.AuditInfo([]string{ClientIP(c), "session %s", "upload avatar", "saved as %s"}, s.RefID, clean.Log(filePath))
|
||||
}
|
||||
|
||||
if mediaFile, mediaErr := photoprism.NewMediaFile(filePath); mediaErr != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "%s"}, s.RefID, err)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrWrongFileType)
|
||||
return
|
||||
} else if err = mediaFile.CreateThumbnails(conf.ThumbCachePath(), false); err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "%s"}, s.RefID, err)
|
||||
} else if err = s.User().SetAvatar(mediaFile.Hash(), entity.SrcManual); err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "%s"}, s.RefID, err)
|
||||
}
|
||||
|
||||
// Clear the session cache, as it contains user information.
|
||||
s.ClearCache()
|
||||
|
||||
log.Info(i18n.Msg(i18n.MsgFileUploaded))
|
||||
|
||||
c.JSON(http.StatusOK, entity.FindUserByUID(uid))
|
||||
})
|
||||
}
|
22
internal/api/users_avatar_test.go
Normal file
22
internal/api/users_avatar_test.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
)
|
||||
|
||||
func TestUploadUserAvatar(t *testing.T) {
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
adminUid := entity.Admin.UserUID
|
||||
reqUrl := fmt.Sprintf("/api/v1/users/%s/avatar", adminUid)
|
||||
UploadUserAvatar(router)
|
||||
r := PerformRequestWithBody(app, "POST", reqUrl, "{foo:123}")
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
}
|
|
@ -15,10 +15,10 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
||||
// ChangePassword changes the password of the currently authenticated user.
|
||||
// UpdateUserPassword changes the password of the currently authenticated user.
|
||||
//
|
||||
// PUT /api/v1/users/:uid/password
|
||||
func ChangePassword(router *gin.RouterGroup) {
|
||||
func UpdateUserPassword(router *gin.RouterGroup) {
|
||||
router.PUT("/users/:uid/password", func(c *gin.Context) {
|
||||
conf := get.Config()
|
||||
|
|
@ -14,7 +14,7 @@ import (
|
|||
func TestChangePassword(t *testing.T) {
|
||||
t.Run("NonExistentUser", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
ChangePassword(router)
|
||||
UpdateUserPassword(router)
|
||||
r := PerformRequestWithBody(app, "PUT", "/api/v1/users/xxx/password", `{}`)
|
||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||
})
|
||||
|
@ -23,7 +23,7 @@ func TestChangePassword(t *testing.T) {
|
|||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
ChangePassword(router)
|
||||
UpdateUserPassword(router)
|
||||
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
|
||||
f := form.ChangePassword{
|
||||
|
@ -43,7 +43,7 @@ func TestChangePassword(t *testing.T) {
|
|||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
ChangePassword(router)
|
||||
UpdateUserPassword(router)
|
||||
|
||||
oldPassword := "PleaseChange$42"
|
||||
newPassword := "SoftwareDevelopmentIsAYoungProfession1234567890!@#$%^&*()_+[]{}|:<>?/.,"
|
||||
|
@ -81,7 +81,7 @@ func TestChangePassword(t *testing.T) {
|
|||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
ChangePassword(router)
|
||||
UpdateUserPassword(router)
|
||||
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
|
||||
f := form.ChangePassword{
|
||||
|
@ -101,7 +101,7 @@ func TestChangePassword(t *testing.T) {
|
|||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
ChangePassword(router)
|
||||
UpdateUserPassword(router)
|
||||
sessId := AuthenticateUser(app, router, "bob", "Bobbob123!")
|
||||
|
||||
f := form.ChangePassword{
|
||||
|
@ -121,7 +121,7 @@ func TestChangePassword(t *testing.T) {
|
|||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
ChangePassword(router)
|
||||
UpdateUserPassword(router)
|
||||
sessId := AuthenticateUser(app, router, "friend", "!Friend321")
|
||||
|
||||
f := form.ChangePassword{
|
||||
|
@ -141,7 +141,7 @@ func TestChangePassword(t *testing.T) {
|
|||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
ChangePassword(router)
|
||||
UpdateUserPassword(router)
|
||||
sessId := AuthenticateUser(app, router, "bob", "Bobbob123!")
|
||||
|
||||
f := form.ChangePassword{
|
82
internal/api/users_update.go
Normal file
82
internal/api/users_update.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
)
|
||||
|
||||
// UpdateUser updates the profile information of the currently authenticated user.
|
||||
//
|
||||
// PUT /api/v1/users/:uid
|
||||
func UpdateUser(router *gin.RouterGroup) {
|
||||
router.PUT("/users/:uid", func(c *gin.Context) {
|
||||
conf := get.Config()
|
||||
|
||||
if conf.Demo() || conf.DisableSettings() {
|
||||
AbortForbidden(c)
|
||||
return
|
||||
}
|
||||
|
||||
s := AuthAny(c, acl.ResourceUsers, acl.Permissions{acl.ActionManage, acl.AccessOwn, acl.ActionUpdate})
|
||||
|
||||
if s.Abort(c) {
|
||||
return
|
||||
}
|
||||
|
||||
uid := clean.UID(c.Param("uid"))
|
||||
|
||||
m := entity.FindUserByUID(uid)
|
||||
|
||||
if m == nil {
|
||||
Abort(c, http.StatusNotFound, i18n.ErrUserNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 1) Init form with model values
|
||||
f, err := form.NewUser(m)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 2) Update form with values from request
|
||||
if err = c.BindJSON(&f); err != nil {
|
||||
log.Error(err)
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 3) Save model with values from form
|
||||
if err = m.SaveForm(f); err != nil {
|
||||
log.Error(err)
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Clear the session cache, as it contains user information.
|
||||
s.ClearCache()
|
||||
|
||||
event.SuccessMsg(i18n.MsgChangesSaved)
|
||||
|
||||
m = entity.FindUserByUID(uid)
|
||||
|
||||
if m == nil {
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, m)
|
||||
})
|
||||
}
|
36
internal/api/users_update_test.go
Normal file
36
internal/api/users_update_test.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
)
|
||||
|
||||
func TestUpdateUser(t *testing.T) {
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
UpdateUser(router)
|
||||
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
adminUid := entity.Admin.UserUID
|
||||
reqUrl := fmt.Sprintf("/api/v1/users/%s", adminUid)
|
||||
t.Logf("Request URL: %s", reqUrl)
|
||||
r := AuthenticatedRequestWithBody(app, "PUT", reqUrl, "{Email:\"admin@example.com\",Details:{Location:\"WebStorm\"}}", sessId)
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
adminUid := entity.Admin.UserUID
|
||||
reqUrl := fmt.Sprintf("/api/v1/users/%s", adminUid)
|
||||
UpdateUser(router)
|
||||
r := PerformRequestWithBody(app, "PUT", reqUrl, "{foo:123}")
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
}
|
245
internal/api/users_upload.go
Normal file
245
internal/api/users_upload.go
Normal file
|
@ -0,0 +1,245 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
||||
// UploadUserFiles adds files to the user upload folder, from where they can be moved and indexed.
|
||||
//
|
||||
// POST /users/:uid/upload/:token
|
||||
func UploadUserFiles(router *gin.RouterGroup) {
|
||||
router.POST("/users/:uid/upload/:token", func(c *gin.Context) {
|
||||
conf := get.Config()
|
||||
|
||||
if conf.ReadOnly() || !conf.Settings().Features.Upload {
|
||||
Abort(c, http.StatusForbidden, i18n.ErrReadOnly)
|
||||
return
|
||||
}
|
||||
|
||||
s := AuthAny(c, acl.ResourceFiles, acl.Permissions{acl.ActionManage, acl.ActionUpload})
|
||||
|
||||
if s.Abort(c) {
|
||||
return
|
||||
}
|
||||
|
||||
// Users may only upload their own files.
|
||||
if s.User().UserUID != clean.UID(c.Param("uid")) {
|
||||
AbortForbidden(c)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
token := clean.Token(c.Param("token"))
|
||||
|
||||
f, err := c.MultipartForm()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("upload: %s", err)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed)
|
||||
return
|
||||
}
|
||||
|
||||
event.Publish("upload.start", event.Data{"time": start})
|
||||
|
||||
files := f.File["files"]
|
||||
uploaded := len(files)
|
||||
|
||||
var uploads []string
|
||||
|
||||
uploadDir, err := conf.UserUploadPath(s.UserUID, s.RefID+token)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("upload: failed to create storage folder (%s)", err)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed)
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
filename := path.Join(uploadDir, filepath.Base(file.Filename))
|
||||
|
||||
log.Debugf("upload: saving file %s", clean.Log(file.Filename))
|
||||
|
||||
if err := c.SaveUploadedFile(file, filename); err != nil {
|
||||
log.Errorf("upload: failed saving file %s", clean.Log(filepath.Base(file.Filename)))
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed)
|
||||
return
|
||||
}
|
||||
|
||||
uploads = append(uploads, filename)
|
||||
}
|
||||
|
||||
if !conf.UploadNSFW() {
|
||||
nd := get.NsfwDetector()
|
||||
|
||||
containsNSFW := false
|
||||
|
||||
for _, filename := range uploads {
|
||||
labels, err := nd.File(filename)
|
||||
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if labels.IsSafe() {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("nsfw: %s might be offensive", clean.Log(filename))
|
||||
|
||||
containsNSFW = true
|
||||
}
|
||||
|
||||
if containsNSFW {
|
||||
for _, filename := range uploads {
|
||||
if err := os.Remove(filename); err != nil {
|
||||
log.Errorf("nsfw: could not delete %s", clean.Log(filename))
|
||||
}
|
||||
}
|
||||
|
||||
Abort(c, http.StatusForbidden, i18n.ErrOffensiveUpload)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := int(time.Since(start).Seconds())
|
||||
|
||||
msg := i18n.Msg(i18n.MsgFilesUploadedIn, uploaded, elapsed)
|
||||
|
||||
log.Info(msg)
|
||||
|
||||
c.JSON(http.StatusOK, i18n.Response{Code: http.StatusOK, Msg: msg})
|
||||
})
|
||||
}
|
||||
|
||||
// ProcessUserUpload triggers processing once all files have been uploaded.
|
||||
//
|
||||
// PUT /users/:uid/upload/:token
|
||||
func ProcessUserUpload(router *gin.RouterGroup) {
|
||||
router.PUT("/users/:uid/upload/:token", func(c *gin.Context) {
|
||||
s := AuthAny(c, acl.ResourceFiles, acl.Permissions{acl.ActionManage, acl.ActionUpload})
|
||||
|
||||
if s.Abort(c) {
|
||||
return
|
||||
}
|
||||
|
||||
// Users may only upload their own files.
|
||||
if s.User().UserUID != clean.UID(c.Param("uid")) {
|
||||
AbortForbidden(c)
|
||||
return
|
||||
}
|
||||
|
||||
conf := get.Config()
|
||||
|
||||
if conf.ReadOnly() || !conf.Settings().Features.Import {
|
||||
AbortFeatureDisabled(c)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
var f form.UploadOptions
|
||||
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
token := clean.Token(c.Param("token"))
|
||||
uploadPath, err := conf.UserUploadPath(s.UserUID, s.RefID+token)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("upload: failed to create storage folder (%s)", err)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed)
|
||||
return
|
||||
}
|
||||
|
||||
imp := get.Import()
|
||||
|
||||
var destFolder string
|
||||
if destFolder = s.User().UploadPath; destFolder == "" {
|
||||
destFolder = conf.ImportDest()
|
||||
}
|
||||
|
||||
// Move uploaded files to the destination folder.
|
||||
event.InfoMsg(i18n.MsgProcessingUpload)
|
||||
opt := photoprism.ImportOptionsUpload(uploadPath, destFolder)
|
||||
|
||||
// Add imported files to albums if allowed.
|
||||
if len(f.Albums) > 0 &&
|
||||
acl.Resources.AllowAny(acl.ResourceAlbums, s.User().AclRole(), acl.Permissions{acl.ActionCreate, acl.ActionUpload}) {
|
||||
log.Debugf("upload: adding files to album %s", clean.Log(strings.Join(f.Albums, " and ")))
|
||||
opt.Albums = f.Albums
|
||||
}
|
||||
|
||||
// Set user UID if known.
|
||||
if s.UserUID != "" {
|
||||
opt.UserUID = s.UserUID
|
||||
}
|
||||
|
||||
// Start import.
|
||||
imported := imp.Start(opt)
|
||||
|
||||
// Delete empty import directory.
|
||||
if fs.DirIsEmpty(uploadPath) {
|
||||
if err := os.Remove(uploadPath); err != nil {
|
||||
log.Errorf("upload: failed deleting empty folder %s: %s", clean.Log(uploadPath), err)
|
||||
} else {
|
||||
log.Infof("upload: deleted empty folder %s", clean.Log(uploadPath))
|
||||
}
|
||||
}
|
||||
|
||||
// Update moments if files have been imported.
|
||||
if n := len(imported); n == 0 {
|
||||
log.Infof("upload: no new files imported", clean.Log(uploadPath))
|
||||
} else {
|
||||
log.Infof("upload: imported %s", english.Plural(n, "file", "files"))
|
||||
if moments := get.Moments(); moments == nil {
|
||||
log.Warnf("upload: moments service not set - possible bug")
|
||||
} else if err := moments.Start(); err != nil {
|
||||
log.Warnf("moments: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := int(time.Since(start).Seconds())
|
||||
|
||||
// Show success message.
|
||||
msg := i18n.Msg(i18n.MsgUploadProcessed)
|
||||
|
||||
event.Success(msg)
|
||||
event.Publish("import.completed", event.Data{"path": uploadPath, "seconds": elapsed})
|
||||
event.Publish("index.completed", event.Data{"path": uploadPath, "seconds": elapsed})
|
||||
|
||||
for _, uid := range f.Albums {
|
||||
PublishAlbumEvent(EntityUpdated, uid, c)
|
||||
}
|
||||
|
||||
// Update the user interface.
|
||||
UpdateClientConfig()
|
||||
|
||||
// Update album, label, and subject cover thumbs.
|
||||
if err := query.UpdateCovers(); err != nil {
|
||||
log.Warnf("upload: %s (update covers)", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, i18n.Response{Code: http.StatusOK, Msg: msg})
|
||||
})
|
||||
}
|
23
internal/api/users_upload_test.go
Normal file
23
internal/api/users_upload_test.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
)
|
||||
|
||||
func TestUploadUserFiles(t *testing.T) {
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
adminUid := entity.Admin.UserUID
|
||||
reqUrl := fmt.Sprintf("/api/v1/users/%s/upload/abc123456789", adminUid)
|
||||
// t.Logf("Request URL: %s", reqUrl)
|
||||
UploadUserFiles(router)
|
||||
r := PerformRequestWithBody(app, "POST", reqUrl, "{foo:123}")
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
}
|
|
@ -10,6 +10,8 @@ import (
|
|||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
@ -87,6 +89,18 @@ func (c *Config) CreateDirectories() error {
|
|||
return createError(c.StoragePath(), err)
|
||||
}
|
||||
|
||||
if c.FilesPath() == "" {
|
||||
return notFoundError("files")
|
||||
} else if err := os.MkdirAll(c.FilesPath(), os.ModePerm); err != nil {
|
||||
return createError(c.FilesPath(), err)
|
||||
}
|
||||
|
||||
if c.UsersPath() == "" {
|
||||
return notFoundError("users")
|
||||
} else if err := os.MkdirAll(c.UsersPath(), os.ModePerm); err != nil {
|
||||
return createError(c.UsersPath(), err)
|
||||
}
|
||||
|
||||
if c.CmdCachePath() == "" {
|
||||
return notFoundError("cmd cache")
|
||||
} else if err := os.MkdirAll(c.CmdCachePath(), os.ModePerm); err != nil {
|
||||
|
@ -296,6 +310,72 @@ func (c *Config) SidecarWritable() bool {
|
|||
return !c.ReadOnly() || c.SidecarPathIsAbs()
|
||||
}
|
||||
|
||||
// FilesPath returns the storage base path for files that should not be indexed.
|
||||
func (c *Config) FilesPath() string {
|
||||
// Set default.
|
||||
if c.options.FilesPath == "" {
|
||||
c.options.FilesPath = filepath.Join(c.StoragePath(), "files")
|
||||
}
|
||||
|
||||
return c.options.FilesPath
|
||||
}
|
||||
|
||||
// FilePath returns the file storage path based on the hash provided.
|
||||
func (c *Config) FilePath(fileHash string) string {
|
||||
if !rnd.IsHex(fileHash) || len(fileHash) < 4 {
|
||||
return ""
|
||||
}
|
||||
|
||||
dir := filepath.Join(c.FilesPath(), fileHash[0:1], fileHash[1:2], fileHash[2:3])
|
||||
|
||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return filepath.Join(dir, fileHash)
|
||||
}
|
||||
|
||||
// UsersPath returns the storage base path for user assets like
|
||||
// avatar images and other media that should not be indexed.
|
||||
func (c *Config) UsersPath() string {
|
||||
// Set default.
|
||||
if c.options.UsersPath == "" {
|
||||
c.options.UsersPath = filepath.Join(c.StoragePath(), "users")
|
||||
}
|
||||
|
||||
return c.options.UsersPath
|
||||
}
|
||||
|
||||
// UserPath returns the storage path for user assets.
|
||||
func (c *Config) UserPath(userUid string) string {
|
||||
if !rnd.IsUID(userUid, 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
dir := filepath.Join(c.UsersPath(), userUid)
|
||||
|
||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
// UserUploadPath returns the upload path for the specified user.
|
||||
func (c *Config) UserUploadPath(userUid, token string) (string, error) {
|
||||
if !rnd.IsUID(userUid, 0) {
|
||||
return "", fmt.Errorf("invalid uid")
|
||||
}
|
||||
|
||||
dir := filepath.Join(c.UserPath(userUid), "upload", clean.Token(token))
|
||||
|
||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// TempPath returns the cached temporary directory name e.g. for uploads and downloads.
|
||||
func (c *Config) TempPath() string {
|
||||
// Return cached value?
|
||||
|
|
|
@ -23,6 +23,53 @@ func TestConfig_SidecarPath(t *testing.T) {
|
|||
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/storage/testdata/sidecar", c.SidecarPath())
|
||||
}
|
||||
|
||||
func TestConfig_FilePath(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
s := c.FilePath("c476503628b4543c9ef97d69a6daa700b05d19bc")
|
||||
assert.True(t, strings.HasSuffix(s, "/c/4/7/c476503628b4543c9ef97d69a6daa700b05d19bc"))
|
||||
})
|
||||
t.Run("InvalidHash", func(t *testing.T) {
|
||||
assert.Equal(t, "", c.FilePath("YE"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfig_UsersPath(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
assert.Contains(t, c.UsersPath(), "testdata/users")
|
||||
}
|
||||
|
||||
func TestConfig_UserPath(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
assert.Equal(t, "", c.UserPath(""))
|
||||
assert.Equal(t, "", c.UserPath("etaetyget"))
|
||||
assert.Contains(t, c.UserPath("urjult03ceelhw6k"), "testdata/users/urjult03ceelhw6k")
|
||||
}
|
||||
|
||||
func TestConfig_UserUploadPath(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
if dir, err := c.UserUploadPath("", ""); err == nil {
|
||||
t.Error("error expected")
|
||||
} else {
|
||||
assert.Equal(t, "", dir)
|
||||
}
|
||||
if dir, err := c.UserUploadPath("etaetyget", ""); err == nil {
|
||||
t.Error("error expected")
|
||||
} else {
|
||||
assert.Equal(t, "", dir)
|
||||
}
|
||||
if dir, err := c.UserUploadPath("urjult03ceelhw6k", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Contains(t, dir, "testdata/users/urjult03ceelhw6k/upload")
|
||||
}
|
||||
if dir, err := c.UserUploadPath("urjult03ceelhw6k", "foo"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Contains(t, dir, "testdata/users/urjult03ceelhw6k/upload/foo")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_SidecarPathIsAbs(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
|
|
|
@ -140,6 +140,11 @@ var Flags = CliFlags{
|
|||
Usage: "custom relative or absolute sidecar `PATH` *optional*",
|
||||
EnvVar: "PHOTOPRISM_SIDECAR_PATH",
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "users-path",
|
||||
Usage: "custom users storage `PATH` *optional*",
|
||||
EnvVar: "PHOTOPRISM_USERS_PATH",
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "backup-path, ba",
|
||||
Usage: "custom backup `PATH` for index backup files *optional*",
|
||||
|
|
|
@ -45,6 +45,8 @@ type Options struct {
|
|||
ResolutionLimit int `yaml:"ResolutionLimit" json:"ResolutionLimit" flag:"resolution-limit"`
|
||||
StoragePath string `yaml:"StoragePath" json:"-" flag:"storage-path"`
|
||||
SidecarPath string `yaml:"SidecarPath" json:"-" flag:"sidecar-path"`
|
||||
FilesPath string `yaml:"FilesPath" json:"-" flag:"files-path"`
|
||||
UsersPath string `yaml:"UsersPath" json:"-" flag:"users-path"`
|
||||
BackupPath string `yaml:"BackupPath" json:"-" flag:"backup-path"`
|
||||
CachePath string `yaml:"CachePath" json:"-" flag:"cache-path"`
|
||||
ImportPath string `yaml:"ImportPath" json:"-" flag:"import-path"`
|
||||
|
|
|
@ -48,6 +48,8 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||
// Other paths.
|
||||
{"storage-path", c.StoragePath()},
|
||||
{"sidecar-path", c.SidecarPath()},
|
||||
{"files-path", c.FilesPath()},
|
||||
{"users-path", c.UsersPath()},
|
||||
{"albums-path", c.AlbumsPath()},
|
||||
{"backup-path", c.BackupPath()},
|
||||
{"cache-path", c.CachePath()},
|
||||
|
|
|
@ -158,6 +158,11 @@ func (m *Session) Cache() {
|
|||
m.CacheDuration(sessionCacheExpiration)
|
||||
}
|
||||
|
||||
// ClearCache deletes the session from the cache.
|
||||
func (m *Session) ClearCache() {
|
||||
DeleteFromSessionCache(m.ID)
|
||||
}
|
||||
|
||||
// Create new entity in the database.
|
||||
func (m *Session) Create() (err error) {
|
||||
if err = Db().Create(m).Error; err == nil && rnd.IsSessionID(m.ID) {
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ulule/deepcopier"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
|
@ -66,8 +68,8 @@ type User struct {
|
|||
ResetToken string `gorm:"type:VARBINARY(64);" json:"-" yaml:"-"`
|
||||
PreviewToken string `gorm:"type:VARBINARY(64);column:preview_token;" json:"-" yaml:"-"`
|
||||
DownloadToken string `gorm:"type:VARBINARY(64);column:download_token;" json:"-" yaml:"-"`
|
||||
Thumb string `gorm:"type:VARBINARY(128);index;default:'';" json:"Thumb,omitempty" yaml:"Thumb,omitempty"`
|
||||
ThumbSrc string `gorm:"type:VARBINARY(8);default:'';" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
|
||||
Thumb string `gorm:"type:VARBINARY(128);index;default:'';" json:"Thumb" yaml:"Thumb,omitempty"`
|
||||
ThumbSrc string `gorm:"type:VARBINARY(8);default:'';" json:"ThumbSrc" yaml:"ThumbSrc,omitempty"`
|
||||
RefID string `gorm:"type:VARBINARY(16);" json:"-" yaml:"-"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
||||
|
@ -764,3 +766,42 @@ func (m *User) RedeemToken(token string) (n int) {
|
|||
|
||||
return n
|
||||
}
|
||||
|
||||
// SaveForm updates the entity using form data and stores it in the database.
|
||||
func (m *User) SaveForm(f form.User) error {
|
||||
if m.UserName == "" || m.ID <= 0 {
|
||||
return fmt.Errorf("system users cannot be updated")
|
||||
}
|
||||
|
||||
if err := deepcopier.Copy(m.UserDetails).From(f.UserDetails); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n := clean.Name(f.DisplayName); n != "" && n != m.DisplayName {
|
||||
m.DisplayName = n
|
||||
}
|
||||
|
||||
if email := clean.Email(f.UserEmail); email != "" && email != m.UserEmail {
|
||||
m.UserEmail = email
|
||||
m.VerifiedAt = nil
|
||||
m.VerifyToken = GenerateToken()
|
||||
}
|
||||
|
||||
return m.Save()
|
||||
}
|
||||
|
||||
// SetAvatar updates the user avatar image.
|
||||
func (m *User) SetAvatar(thumb, thumbSrc string) error {
|
||||
if m.UserName == "" || m.ID <= 0 {
|
||||
return fmt.Errorf("system user avatars cannot be changed")
|
||||
}
|
||||
|
||||
if SrcPriority[thumbSrc] < SrcPriority[m.ThumbSrc] && m.Thumb != "" {
|
||||
return fmt.Errorf("no permission to change avatar")
|
||||
}
|
||||
|
||||
m.Thumb = thumb
|
||||
m.ThumbSrc = thumbSrc
|
||||
|
||||
return m.Updates(Values{"Thumb": m.Thumb, "ThumbSrc": m.ThumbSrc})
|
||||
}
|
||||
|
|
|
@ -16,36 +16,38 @@ const (
|
|||
|
||||
// UserDetails represents user profile information.
|
||||
type UserDetails struct {
|
||||
UserUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"-" yaml:"UserUID"`
|
||||
SubjUID string `gorm:"type:VARBINARY(42);index;" json:"SubjUID,omitempty" yaml:"SubjUID,omitempty"`
|
||||
SubjSrc string `gorm:"type:VARBINARY(8);default:'';" json:"SubjSrc,omitempty" yaml:"SubjSrc,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,omitempty" yaml:"CellID,omitempty"`
|
||||
IdURL string `gorm:"type:VARBINARY(512);column:id_url;" json:"IdURL,omitempty" yaml:"IdURL,omitempty"`
|
||||
AvatarURL string `gorm:"type:VARBINARY(512);column:avatar_url" json:"AvatarURL,omitempty" yaml:"AvatarURL,omitempty"`
|
||||
SiteURL string `gorm:"type:VARBINARY(512);column:site_url" json:"SiteURL,omitempty" yaml:"SiteURL,omitempty"`
|
||||
FeedURL string `gorm:"type:VARBINARY(512);column:feed_url" json:"FeedURL,omitempty" yaml:"FeedURL,omitempty"`
|
||||
UserGender string `gorm:"size:16;" json:"Gender,omitempty" yaml:"Gender,omitempty"`
|
||||
NamePrefix string `gorm:"size:32;" json:"NamePrefix,omitempty" yaml:"NamePrefix,omitempty"`
|
||||
GivenName string `gorm:"size:64;" json:"GivenName,omitempty" yaml:"GivenName,omitempty"`
|
||||
MiddleName string `gorm:"size:64;" json:"MiddleName,omitempty" yaml:"MiddleName,omitempty"`
|
||||
FamilyName string `gorm:"size:64;" json:"FamilyName,omitempty" yaml:"FamilyName,omitempty"`
|
||||
NameSuffix string `gorm:"size:32;" json:"NameSuffix,omitempty" yaml:"NameSuffix,omitempty"`
|
||||
NickName string `gorm:"size:64;" json:"NickName,omitempty" yaml:"NickName,omitempty"`
|
||||
UserPhone string `gorm:"size:32;" json:"Phone,omitempty" yaml:"Phone,omitempty"`
|
||||
UserAddress string `gorm:"size:512;" json:"Address,omitempty" yaml:"Address,omitempty"`
|
||||
UserCountry string `gorm:"type:VARBINARY(2);" json:"Country,omitempty" yaml:"Country,omitempty"`
|
||||
UserBio string `gorm:"size:512;" json:"Bio,omitempty" yaml:"Bio,omitempty"`
|
||||
JobTitle string `gorm:"size:64;" json:"JobTitle,omitempty" yaml:"JobTitle,omitempty"`
|
||||
Department string `gorm:"size:128;" json:"Department,omitempty" yaml:"Department,omitempty"`
|
||||
Company string `gorm:"size:128;" json:"Company,omitempty" yaml:"Company,omitempty"`
|
||||
CompanyURL string `gorm:"type:VARBINARY(512);column:company_url" json:"CompanyURL,omitempty" yaml:"CompanyURL,omitempty"`
|
||||
BirthYear int `gorm:"default:-1;" json:"BirthYear,omitempty" yaml:"BirthYear,omitempty"`
|
||||
BirthMonth int `gorm:"default:-1;" json:"BirthMonth,omitempty" yaml:"BirthMonth,omitempty"`
|
||||
BirthDay int `gorm:"default:-1;" json:"BirthDay,omitempty" yaml:"BirthDay,omitempty"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
||||
UserUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"-" yaml:"-"`
|
||||
IdURL string `gorm:"type:VARBINARY(512);column:id_url;" json:"IdURL,omitempty" yaml:"IdURL,omitempty"`
|
||||
SubjUID string `gorm:"type:VARBINARY(42);index;" json:"SubjUID,omitempty" yaml:"SubjUID,omitempty"`
|
||||
SubjSrc string `gorm:"type:VARBINARY(8);default:'';" json:"-" yaml:"SubjSrc,omitempty"`
|
||||
PlaceID string `gorm:"type:VARBINARY(42);index;default:'zz'" json:"-" yaml:"-"`
|
||||
PlaceSrc string `gorm:"type:VARBINARY(8);" json:"-" yaml:"PlaceSrc,omitempty"`
|
||||
CellID string `gorm:"type:VARBINARY(42);index;default:'zz'" json:"-" yaml:"CellID,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"`
|
||||
NamePrefix string `gorm:"size:32;" json:"NamePrefix" yaml:"NamePrefix,omitempty"`
|
||||
GivenName string `gorm:"size:64;" json:"GivenName" yaml:"GivenName,omitempty"`
|
||||
MiddleName string `gorm:"size:64;" json:"MiddleName" yaml:"MiddleName,omitempty"`
|
||||
FamilyName string `gorm:"size:64;" json:"FamilyName" yaml:"FamilyName,omitempty"`
|
||||
NameSuffix string `gorm:"size:32;" json:"NameSuffix" yaml:"NameSuffix,omitempty"`
|
||||
NickName string `gorm:"size:64;" json:"NickName" yaml:"NickName,omitempty"`
|
||||
UserGender string `gorm:"size:16;" json:"Gender" yaml:"Gender,omitempty"`
|
||||
UserBio string `gorm:"size:512;" json:"Bio" yaml:"Bio,omitempty"`
|
||||
UserLocation string `gorm:"size:512;" json:"Location" yaml:"Location,omitempty"`
|
||||
UserCountry string `gorm:"type:VARBINARY(2);" json:"Country" yaml:"Country,omitempty"`
|
||||
UserPhone string `gorm:"size:32;" json:"Phone" yaml:"Phone,omitempty"`
|
||||
SiteURL string `gorm:"type:VARBINARY(512);column:site_url" json:"SiteURL" yaml:"SiteURL,omitempty"`
|
||||
ProfileURL string `gorm:"type:VARBINARY(512);column:profile_url" json:"ProfileURL" yaml:"ProfileURL,omitempty"`
|
||||
FeedURL string `gorm:"type:VARBINARY(512);column:feed_url" json:"FeedURL,omitempty" yaml:"FeedURL,omitempty"`
|
||||
AvatarURL string `gorm:"type:VARBINARY(512);column:avatar_url" json:"AvatarURL,omitempty" yaml:"AvatarURL,omitempty"`
|
||||
OrgName string `gorm:"size:128;" json:"OrgName" yaml:"OrgName,omitempty"`
|
||||
OrgTitle string `gorm:"size:64;" json:"OrgTitle" yaml:"OrgTitle,omitempty"`
|
||||
OrgEmail string `gorm:"size:255;index;" json:"OrgEmail" yaml:"OrgEmail,omitempty"`
|
||||
OrgPhone string `gorm:"size:32;" json:"OrgPhone" yaml:"OrgPhone,omitempty"`
|
||||
OrgURL string `gorm:"type:VARBINARY(512);column:org_url" json:"OrgURL" yaml:"OrgURL,omitempty"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the entity table name.
|
||||
|
|
|
@ -735,3 +735,49 @@ func TestUser_SharedUIDs(t *testing.T) {
|
|||
assert.Equal(t, UIDs{"at9lxuqxpogaaba9"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_SaveForm(t *testing.T) {
|
||||
t.Run("UnknownUser", func(t *testing.T) {
|
||||
frm, err := form.NewUser(UnknownUser)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = UnknownUser.SaveForm(frm)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("Admin", func(t *testing.T) {
|
||||
frm, err := form.NewUser(Admin)
|
||||
assert.NoError(t, err)
|
||||
|
||||
frm.UserEmail = "admin@example.com"
|
||||
frm.UserDetails.UserLocation = "GoLand"
|
||||
err = Admin.SaveForm(frm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "admin@example.com", Admin.UserEmail)
|
||||
assert.Equal(t, "GoLand", Admin.Details().UserLocation)
|
||||
|
||||
m := FindUserByUID(Admin.UserUID)
|
||||
assert.Equal(t, "admin@example.com", m.UserEmail)
|
||||
assert.Equal(t, "GoLand", m.Details().UserLocation)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_SetAvatar(t *testing.T) {
|
||||
t.Run("Visitor", func(t *testing.T) {
|
||||
err := Visitor.SetAvatar("ebfc0aea7d3fd018b5fff57c76806b35181855ed", SrcManual)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("UnknownUser", func(t *testing.T) {
|
||||
err := UnknownUser.SetAvatar("ebfc0aea7d3fd018b5fff57c76806b35181855ed", SrcManual)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("Admin", func(t *testing.T) {
|
||||
err := Admin.SetAvatar("ebfc0aea7d3fd018b5fff57c76806b35181855ed", SrcManual)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ebfc0aea7d3fd018b5fff57c76806b35181855ed", Admin.Thumb)
|
||||
assert.Equal(t, SrcManual, Admin.ThumbSrc)
|
||||
|
||||
m := FindUserByUID(Admin.UserUID)
|
||||
assert.Equal(t, "ebfc0aea7d3fd018b5fff57c76806b35181855ed", m.Thumb)
|
||||
assert.Equal(t, SrcManual, m.ThumbSrc)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -104,10 +104,10 @@ class auth_users_details {
|
|||
varbinary(42) place_id
|
||||
varbinary(8) place_src
|
||||
varbinary(42) cell_id
|
||||
varbinary(512) id_url
|
||||
varbinary(512) avatar_url
|
||||
varbinary(512) site_url
|
||||
varbinary(512) feed_url
|
||||
varchar(512) user_bio
|
||||
varchar(32) user_phone
|
||||
varchar(512) user_location
|
||||
varbinary(2) user_country
|
||||
varchar(16) user_gender
|
||||
varchar(32) name_prefix
|
||||
varchar(64) given_name
|
||||
|
@ -115,14 +115,13 @@ class auth_users_details {
|
|||
varchar(64) family_name
|
||||
varchar(32) name_suffix
|
||||
varchar(64) nick_name
|
||||
varchar(32) user_phone
|
||||
varchar(512) user_address
|
||||
varbinary(2) user_country
|
||||
varchar(512) user_bio
|
||||
varchar(64) job_title
|
||||
varchar(128) department
|
||||
varchar(128) company
|
||||
varbinary(512) company_url
|
||||
varchar(64) org_title
|
||||
varchar(128) org_name
|
||||
varbinary(512) org_url
|
||||
varbinary(512) id_url
|
||||
varbinary(512) site_url
|
||||
varbinary(512) feed_url
|
||||
varbinary(512) avatar_url
|
||||
int(11) birth_year
|
||||
int(11) birth_month
|
||||
int(11) birth_day
|
||||
|
|
|
@ -158,10 +158,10 @@ CREATE TABLE `auth_users_details` (
|
|||
`place_id` varbinary(42) DEFAULT 'zz',
|
||||
`place_src` varbinary(8) DEFAULT NULL,
|
||||
`cell_id` varbinary(42) DEFAULT 'zz',
|
||||
`id_url` varbinary(512) DEFAULT NULL,
|
||||
`avatar_url` varbinary(512) DEFAULT NULL,
|
||||
`site_url` varbinary(512) DEFAULT NULL,
|
||||
`feed_url` varbinary(512) DEFAULT NULL,
|
||||
`user_bio` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`user_phone` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`user_location` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`user_country` varbinary(2) DEFAULT NULL,
|
||||
`user_gender` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`name_prefix` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`given_name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
|
@ -169,14 +169,13 @@ CREATE TABLE `auth_users_details` (
|
|||
`family_name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`name_suffix` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`nick_name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`user_phone` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`user_address` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`user_country` varbinary(2) DEFAULT NULL,
|
||||
`user_bio` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`job_title` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`department` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`company` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`company_url` varbinary(512) DEFAULT NULL,
|
||||
`org_title` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`org_name` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`org_url` varbinary(512) DEFAULT NULL,
|
||||
`id_url` varbinary(512) DEFAULT NULL,
|
||||
`site_url` varbinary(512) DEFAULT NULL,
|
||||
`feed_url` varbinary(512) DEFAULT NULL,
|
||||
`avatar_url` varbinary(512) DEFAULT NULL,
|
||||
`birth_year` int(11) DEFAULT -1,
|
||||
`birth_month` int(11) DEFAULT -1,
|
||||
`birth_day` int(11) DEFAULT -1,
|
||||
|
|
|
@ -18,6 +18,7 @@ const (
|
|||
SrcMeta = "meta" // Prio 16
|
||||
SrcXmp = "xmp" // Prio 32
|
||||
SrcManual = "manual" // Prio 64
|
||||
SrcAdmin = "admin" // Prio 128
|
||||
)
|
||||
|
||||
// SrcString returns a source string for logging.
|
||||
|
@ -43,4 +44,5 @@ var SrcPriority = Priorities{
|
|||
SrcMeta: 16,
|
||||
SrcXmp: 32,
|
||||
SrcManual: 64,
|
||||
SrcAdmin: 128,
|
||||
}
|
||||
|
|
5
internal/form/upload_options.go
Normal file
5
internal/form/upload_options.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package form
|
||||
|
||||
type UploadOptions struct {
|
||||
Albums []string `json:"albums"`
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package form
|
||||
|
||||
import (
|
||||
"github.com/ulule/deepcopier"
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
|
@ -8,17 +9,24 @@ import (
|
|||
|
||||
// User represents a user account form.
|
||||
type User struct {
|
||||
UserName string `json:"Name" yaml:"Name,omitempty"`
|
||||
UserEmail string `json:"Email,omitempty" yaml:"Email,omitempty"`
|
||||
DisplayName string `json:"DisplayName,omitempty" yaml:"DisplayName,omitempty"`
|
||||
UserRole string `json:"Role,omitempty" yaml:"Role,omitempty"`
|
||||
SuperAdmin bool `json:"SuperAdmin,omitempty" yaml:"SuperAdmin,omitempty"`
|
||||
CanLogin bool `json:"CanLogin,omitempty" yaml:"CanLogin,omitempty"`
|
||||
WebDAV bool `json:"WebDAV,omitempty" yaml:"WebDAV,omitempty"`
|
||||
UserAttr string `json:"Attr,omitempty" yaml:"Attr,omitempty"`
|
||||
BasePath string `json:"BasePath,omitempty" yaml:"BasePath,omitempty"`
|
||||
UploadPath string `json:"UploadPath,omitempty" yaml:"UploadPath,omitempty"`
|
||||
Password string `json:"Password,omitempty" yaml:"Password,omitempty"`
|
||||
UserName string `json:"Name" yaml:"Name,omitempty"`
|
||||
UserEmail string `json:"Email,omitempty" yaml:"Email,omitempty"`
|
||||
DisplayName string `json:"DisplayName,omitempty" yaml:"DisplayName,omitempty"`
|
||||
UserRole string `json:"Role,omitempty" yaml:"Role,omitempty"`
|
||||
SuperAdmin bool `json:"SuperAdmin,omitempty" yaml:"SuperAdmin,omitempty"`
|
||||
CanLogin bool `json:"CanLogin,omitempty" yaml:"CanLogin,omitempty"`
|
||||
WebDAV bool `json:"WebDAV,omitempty" yaml:"WebDAV,omitempty"`
|
||||
UserAttr string `json:"Attr,omitempty" yaml:"Attr,omitempty"`
|
||||
BasePath string `json:"BasePath,omitempty" yaml:"BasePath,omitempty"`
|
||||
UploadPath string `json:"UploadPath,omitempty" yaml:"UploadPath,omitempty"`
|
||||
Password string `json:"Password,omitempty" yaml:"Password,omitempty"`
|
||||
UserDetails UserDetails `json:"Details"`
|
||||
}
|
||||
|
||||
// NewUser creates a new user account form.
|
||||
func NewUser(m interface{}) (f User, err error) {
|
||||
err = deepcopier.Copy(m).To(&f)
|
||||
return f, err
|
||||
}
|
||||
|
||||
// NewUserFromCli creates a new form with values from a CLI context.
|
||||
|
|
27
internal/form/user_details.go
Normal file
27
internal/form/user_details.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package form
|
||||
|
||||
// UserDetails represents a user details form.
|
||||
type UserDetails struct {
|
||||
BirthYear int `json:"BirthYear"`
|
||||
BirthMonth int `json:"BirthMonth"`
|
||||
BirthDay int `json:"BirthDay"`
|
||||
NamePrefix string `json:"NamePrefix"`
|
||||
GivenName string `json:"GivenName"`
|
||||
MiddleName string `json:"MiddleName"`
|
||||
FamilyName string `json:"FamilyName"`
|
||||
NameSuffix string `json:"NameSuffix"`
|
||||
NickName string `json:"NickName"`
|
||||
UserGender string `json:"Gender"`
|
||||
UserBio string `json:"Bio"`
|
||||
UserLocation string `json:"Location"`
|
||||
UserCountry string `json:"Country"`
|
||||
UserPhone string `json:"Phone"`
|
||||
SiteURL string `json:"SiteURL"`
|
||||
ProfileURL string `json:"ProfileURL"`
|
||||
FeedURL string `json:"FeedURL"`
|
||||
OrgName string `json:"OrgName"`
|
||||
OrgTitle string `json:"OrgTitle"`
|
||||
OrgEmail string `json:"OrgEmail"`
|
||||
OrgPhone string `json:"OrgPhone"`
|
||||
OrgURL string `json:"OrgURL"`
|
||||
}
|
|
@ -14,6 +14,19 @@ func TestUser(t *testing.T) {
|
|||
assert.Equal(t, "passwd", form.Password)
|
||||
}
|
||||
|
||||
func TestNewUser(t *testing.T) {
|
||||
val := &User{UserName: "foobar", UserEmail: "test@test.com", Password: "passwd"}
|
||||
form, err := NewUser(val)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "foobar", form.UserName)
|
||||
assert.Equal(t, "test@test.com", form.UserEmail)
|
||||
assert.Equal(t, "passwd", form.Password)
|
||||
}
|
||||
|
||||
func TestUser_Name(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
form := &User{UserName: "", UserEmail: "test@test.com", Password: "passwd"}
|
||||
|
|
|
@ -8,6 +8,8 @@ const (
|
|||
ErrAlreadyExists
|
||||
ErrNotFound
|
||||
ErrFileNotFound
|
||||
ErrFileTooLarge
|
||||
ErrWrongFileType
|
||||
ErrOriginalsEmpty
|
||||
ErrSelectionNotFound
|
||||
ErrEntityNotFound
|
||||
|
@ -23,6 +25,7 @@ const (
|
|||
ErrUnauthorized
|
||||
ErrForbidden
|
||||
ErrOffensiveUpload
|
||||
ErrUploadFailed
|
||||
ErrNoItemsSelected
|
||||
ErrCreateFile
|
||||
ErrCreateFolder
|
||||
|
@ -72,7 +75,10 @@ const (
|
|||
MsgSubjectDeleted
|
||||
MsgPersonSaved
|
||||
MsgPersonDeleted
|
||||
MsgFileUploaded
|
||||
MsgFilesUploadedIn
|
||||
MsgProcessingUpload
|
||||
MsgUploadProcessed
|
||||
MsgSelectionApproved
|
||||
MsgSelectionArchived
|
||||
MsgSelectionRestored
|
||||
|
@ -92,6 +98,8 @@ var Messages = MessageMap{
|
|||
ErrAlreadyExists: gettext("%s already exists"),
|
||||
ErrNotFound: gettext("Not found"),
|
||||
ErrFileNotFound: gettext("File not found"),
|
||||
ErrFileTooLarge: gettext("File too large"),
|
||||
ErrWrongFileType: gettext("Wrong file type"),
|
||||
ErrOriginalsEmpty: gettext("Originals folder is empty"),
|
||||
ErrSelectionNotFound: gettext("Selection not found"),
|
||||
ErrEntityNotFound: gettext("Entity not found"),
|
||||
|
@ -107,6 +115,7 @@ var Messages = MessageMap{
|
|||
ErrUnauthorized: gettext("Please log in to your account"),
|
||||
ErrForbidden: gettext("Permission denied"),
|
||||
ErrOffensiveUpload: gettext("Upload might be offensive"),
|
||||
ErrUploadFailed: gettext("Upload failed"),
|
||||
ErrNoItemsSelected: gettext("No items selected"),
|
||||
ErrCreateFile: gettext("RunFailed creating file, please check permissions"),
|
||||
ErrCreateFolder: gettext("RunFailed creating folder, please check permissions"),
|
||||
|
@ -157,7 +166,10 @@ var Messages = MessageMap{
|
|||
MsgSubjectDeleted: gettext("Subject deleted"),
|
||||
MsgPersonSaved: gettext("Person saved"),
|
||||
MsgPersonDeleted: gettext("Person deleted"),
|
||||
MsgFileUploaded: gettext("File uploaded"),
|
||||
MsgFilesUploadedIn: gettext("%d files uploaded in %d s"),
|
||||
MsgProcessingUpload: gettext("Processing upload..."),
|
||||
MsgUploadProcessed: gettext("Upload has been processed"),
|
||||
MsgSelectionApproved: gettext("Selection approved"),
|
||||
MsgSelectionArchived: gettext("Selection archived"),
|
||||
MsgSelectionRestored: gettext("Selection restored"),
|
||||
|
|
|
@ -5,6 +5,7 @@ type ImportOptions struct {
|
|||
Albums []string
|
||||
Path string
|
||||
Move bool
|
||||
NonBlocking bool
|
||||
UserUID string
|
||||
DestFolder string
|
||||
RemoveDotFiles bool
|
||||
|
@ -17,6 +18,7 @@ func ImportOptionsCopy(importPath, destFolder string) ImportOptions {
|
|||
result := ImportOptions{
|
||||
Path: importPath,
|
||||
Move: false,
|
||||
NonBlocking: false,
|
||||
DestFolder: destFolder,
|
||||
RemoveDotFiles: false,
|
||||
RemoveExistingFiles: false,
|
||||
|
@ -31,6 +33,22 @@ func ImportOptionsMove(importPath, destFolder string) ImportOptions {
|
|||
result := ImportOptions{
|
||||
Path: importPath,
|
||||
Move: true,
|
||||
NonBlocking: false,
|
||||
DestFolder: destFolder,
|
||||
RemoveDotFiles: true,
|
||||
RemoveExistingFiles: true,
|
||||
RemoveEmptyDirectories: true,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ImportOptionsUpload returns options for importing user uploads.
|
||||
func ImportOptionsUpload(uploadPath, destFolder string) ImportOptions {
|
||||
result := ImportOptions{
|
||||
Path: uploadPath,
|
||||
Move: true,
|
||||
NonBlocking: true,
|
||||
DestFolder: destFolder,
|
||||
RemoveDotFiles: true,
|
||||
RemoveExistingFiles: true,
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/mutex"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
|
|
@ -67,6 +67,7 @@ func ThumbHashMap() (result HashMap, err error) {
|
|||
entity.Label{}.TableName(),
|
||||
entity.Marker{}.TableName(),
|
||||
entity.Subject{}.TableName(),
|
||||
entity.User{}.TableName(),
|
||||
}
|
||||
|
||||
result = make(HashMap)
|
||||
|
|
|
@ -56,7 +56,12 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
// JSON-REST API Version 1
|
||||
v1 := router.Group(conf.BaseUri(config.ApiUri))
|
||||
{
|
||||
// Global App Config.
|
||||
// Authentication.
|
||||
api.CreateSession(v1)
|
||||
api.GetSession(v1)
|
||||
api.DeleteSession(v1)
|
||||
|
||||
// Global Config.
|
||||
api.GetConfigOptions(v1)
|
||||
api.SaveConfigOptions(v1)
|
||||
|
||||
|
@ -65,13 +70,14 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.GetSettings(v1)
|
||||
api.SaveSettings(v1)
|
||||
|
||||
// User Authentication.
|
||||
api.ChangePassword(v1)
|
||||
api.CreateSession(v1)
|
||||
api.GetSession(v1)
|
||||
api.DeleteSession(v1)
|
||||
// Profile and Uploads.
|
||||
api.UploadUserFiles(v1)
|
||||
api.ProcessUserUpload(v1)
|
||||
api.UploadUserAvatar(v1)
|
||||
api.UpdateUserPassword(v1)
|
||||
api.UpdateUser(v1)
|
||||
|
||||
// External Account Management.
|
||||
// Service Accounts.
|
||||
api.SearchServices(v1)
|
||||
api.GetService(v1)
|
||||
api.GetServiceFolders(v1)
|
||||
|
@ -80,21 +86,24 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.DeleteService(v1)
|
||||
api.UpdateService(v1)
|
||||
|
||||
// Thumbs, Downloads, and Videos.
|
||||
// Thumbnail Images.
|
||||
api.GetThumb(v1)
|
||||
api.GetDownload(v1)
|
||||
|
||||
// Video Streaming.
|
||||
api.GetVideo(v1)
|
||||
|
||||
// Downloads.
|
||||
api.GetDownload(v1)
|
||||
api.ZipCreate(v1)
|
||||
api.ZipDownload(v1)
|
||||
|
||||
// Index, Upload, and Import.
|
||||
api.Upload(v1)
|
||||
// Index and Import.
|
||||
api.StartImport(v1)
|
||||
api.CancelImport(v1)
|
||||
api.StartIndexing(v1)
|
||||
api.CancelIndexing(v1)
|
||||
|
||||
// Search & Organization.
|
||||
// Photo Search and Organization.
|
||||
api.SearchPhotos(v1)
|
||||
api.SearchGeo(v1)
|
||||
api.GetPhoto(v1)
|
||||
|
|
Loading…
Reference in a new issue