Auth: Improve account management page and config options #98
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
caa14e621c
commit
f94ff54cc1
68 changed files with 1257 additions and 964 deletions
5
Makefile
5
Makefile
|
@ -97,7 +97,7 @@ install:
|
|||
mkdir --mode=$(INSTALL_MODE) -p $(DESTDIR)
|
||||
env TMPDIR="$(BUILD_PATH)" ./scripts/dist/install-tensorflow.sh $(DESTDIR)
|
||||
rm -rf --preserve-root $(DESTDIR)/include
|
||||
(cd $(DESTDIR) && mkdir -p bin sbin lib assets config config/examples)
|
||||
(cd $(DESTDIR) && mkdir -p bin lib assets config config/examples)
|
||||
./scripts/build.sh prod "$(DESTDIR)/bin/$(BINARY_NAME)"
|
||||
rsync -r -l --safe-links --exclude-from=assets/.buildignore --chmod=a+r,u+rw ./assets/ $(DESTDIR)/assets
|
||||
wget -O $(DESTDIR)/assets/static/img/wallpaper/welcome.jpg https://cdn.photoprism.app/wallpaper/welcome.jpg
|
||||
|
@ -153,6 +153,9 @@ generate:
|
|||
git checkout -- assets/locales/messages.pot;\
|
||||
echo "Reverted unnecessary change in assets/locales/messages.pot.";\
|
||||
fi
|
||||
go-generate:
|
||||
go generate ./pkg/... ./internal/...
|
||||
go fmt ./pkg/... ./internal/...
|
||||
clean-local-assets:
|
||||
rm -rf $(BUILD_PATH)/assets/*
|
||||
clean-local-cache:
|
||||
|
|
BIN
assets/static/img/avatar/tile_100.jpg
Normal file
BIN
assets/static/img/avatar/tile_100.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/static/img/avatar/tile_224.jpg
Normal file
BIN
assets/static/img/avatar/tile_224.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
assets/static/img/avatar/tile_50.jpg
Normal file
BIN
assets/static/img/avatar/tile_50.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 859 B |
BIN
assets/static/img/avatar/tile_500.jpg
Normal file
BIN
assets/static/img/avatar/tile_500.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.4 KiB |
|
@ -19,6 +19,7 @@ services:
|
|||
- "40000:40000" # Go Debugger (host:container)
|
||||
shm_size: "2gb"
|
||||
links:
|
||||
- "traefik:localssl.dev"
|
||||
- "traefik:app.localssl.dev"
|
||||
- "traefik:keycloak.localssl.dev"
|
||||
- "traefik:dummy-webdav.localssl.dev"
|
||||
|
@ -27,7 +28,7 @@ services:
|
|||
- "traefik.enable=true"
|
||||
- "traefik.http.services.photoprism.loadbalancer.server.port=2342"
|
||||
- "traefik.http.routers.photoprism.entrypoints=websecure"
|
||||
- "traefik.http.routers.photoprism.rule=Host(`localssl.dev`, `app.localssl.dev`, `photoprism.localssl.dev`)"
|
||||
- "traefik.http.routers.photoprism.rule=Host(`localssl.dev`, `app.localssl.dev`)"
|
||||
- "traefik.http.routers.photoprism.tls.domains[0].main=localssl.dev"
|
||||
- "traefik.http.routers.photoprism.tls.domains[0].sans=*.localssl.dev"
|
||||
- "traefik.http.routers.photoprism.tls=true"
|
||||
|
@ -148,6 +149,7 @@ services:
|
|||
image: quay.io/keycloak/keycloak:19.0
|
||||
command: "start-dev" # development mode, do not use this in production!
|
||||
links:
|
||||
- "traefik:localssl.dev"
|
||||
- "traefik:app.localssl.dev"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
|
|
|
@ -56,7 +56,7 @@ import * as offline from "@lcdp/offline-plugin/runtime";
|
|||
|
||||
config.progress(50);
|
||||
|
||||
config.load().finally(() => {
|
||||
config.update().finally(() => {
|
||||
// Initialize libs and framework.
|
||||
config.progress(66);
|
||||
const viewer = new Viewer();
|
||||
|
|
|
@ -41,15 +41,15 @@ const testConfig = {
|
|||
manifestUri: "/manifest.json?0e41a7e5",
|
||||
};
|
||||
|
||||
const config = window.__CONFIG__ ? window.__CONFIG__ : testConfig;
|
||||
const c = window.__CONFIG__ ? window.__CONFIG__ : testConfig;
|
||||
|
||||
const Api = Axios.create({
|
||||
baseURL: config.apiUri,
|
||||
baseURL: c.apiUri,
|
||||
headers: {
|
||||
common: {
|
||||
"X-Session-ID": window.localStorage.getItem("session_id"),
|
||||
"X-Client-Uri": config.jsUri,
|
||||
"X-Client-Version": config.version,
|
||||
"X-Client-Uri": c.jsUri,
|
||||
"X-Client-Version": c.version,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -75,17 +75,12 @@ Api.interceptors.response.use(
|
|||
console.warn("WARNING: Server returned HTML instead of JSON - API not implemented?");
|
||||
}
|
||||
|
||||
// Update preview token.
|
||||
if (resp.headers && resp.headers["x-preview-token"]) {
|
||||
const previewToken = resp.headers["x-preview-token"];
|
||||
const downloadToken = resp.headers["x-download-token"]
|
||||
? resp.headers["x-download-token"]
|
||||
: "";
|
||||
if (config.previewToken !== previewToken) {
|
||||
config.previewToken = previewToken;
|
||||
config.downloadToken = downloadToken;
|
||||
Event.publish("config.updated", { config: { previewToken, downloadToken } });
|
||||
}
|
||||
// Update tokens if provided.
|
||||
if (resp.headers && resp.headers["x-preview-token"] && resp.headers["x-download-token"]) {
|
||||
Event.publish("config.tokens", {
|
||||
previewToken: resp.headers["x-preview-token"],
|
||||
downloadToken: resp.headers["x-download-token"],
|
||||
});
|
||||
}
|
||||
|
||||
return resp;
|
||||
|
|
|
@ -44,6 +44,7 @@ export default class Config {
|
|||
this.storage_key = "config";
|
||||
this.previewToken = "";
|
||||
this.downloadToken = "";
|
||||
this.updating = false;
|
||||
|
||||
this.$vuetify = null;
|
||||
this.translations = translations;
|
||||
|
@ -62,7 +63,10 @@ export default class Config {
|
|||
this.staticUri = "/static";
|
||||
this.apiUri = "/api/v1";
|
||||
this.contentUri = this.apiUri;
|
||||
this.values = {};
|
||||
this.values = {
|
||||
mode: "test",
|
||||
name: "Test",
|
||||
};
|
||||
this.page = {
|
||||
title: "PhotoPrism",
|
||||
caption: "AI-Powered Photos App",
|
||||
|
@ -106,6 +110,7 @@ export default class Config {
|
|||
this.updateTokens();
|
||||
|
||||
Event.subscribe("config.updated", (ev, data) => this.setValues(data.config));
|
||||
Event.subscribe("config.tokens", (ev, data) => this.setTokens(data));
|
||||
Event.subscribe("count", (ev, data) => this.onCount(ev, data));
|
||||
Event.subscribe("people", (ev, data) => this.onPeople(ev, data));
|
||||
|
||||
|
@ -125,16 +130,27 @@ export default class Config {
|
|||
return this.update();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
update() {
|
||||
return Api.get("config")
|
||||
if (this.updating !== false) {
|
||||
return this.updating;
|
||||
}
|
||||
|
||||
this.updating = Api.get("config")
|
||||
.then(
|
||||
(response) => this.setValues(response.data),
|
||||
(resp) => {
|
||||
return this.setValues(resp.data);
|
||||
},
|
||||
() => console.warn("config update failed")
|
||||
)
|
||||
.finally(() => Promise.resolve());
|
||||
.finally(() => {
|
||||
this.updating = false;
|
||||
return Promise.resolve(this);
|
||||
});
|
||||
|
||||
return this.updating;
|
||||
}
|
||||
|
||||
setValues(values) {
|
||||
|
@ -525,6 +541,16 @@ export default class Config {
|
|||
return Languages().some((lang) => lang.value === this.values.settings.ui.language && lang.rtl);
|
||||
}
|
||||
|
||||
setTokens(tokens) {
|
||||
if (!tokens || !tokens.previewToken || !tokens.downloadToken) {
|
||||
return;
|
||||
}
|
||||
this.previewToken = tokens.previewToken;
|
||||
this.values.previewToken = tokens.previewToken;
|
||||
this.downloadToken = tokens.downloadToken;
|
||||
this.values.downloadToken = tokens.downloadToken;
|
||||
}
|
||||
|
||||
updateTokens() {
|
||||
if (this.values["previewToken"]) {
|
||||
this.previewToken = this.values.previewToken;
|
||||
|
|
|
@ -29,6 +29,7 @@ import User from "model/user";
|
|||
import Socket from "websocket.js";
|
||||
|
||||
const SessionHeader = "X-Session-ID";
|
||||
const PublicID = "234200000000000000000000000000000000000000000000";
|
||||
|
||||
export default class Session {
|
||||
/**
|
||||
|
@ -297,7 +298,16 @@ export default class Session {
|
|||
}
|
||||
|
||||
refresh() {
|
||||
if (this.hasId() && !this.config.isPublic()) {
|
||||
// Refresh session information.
|
||||
if (this.config.isPublic()) {
|
||||
// No authentication in public mode.
|
||||
this.setId(PublicID);
|
||||
return Api.get("session/" + this.getId()).then((resp) => {
|
||||
this.setResp(resp);
|
||||
return Promise.resolve();
|
||||
});
|
||||
} else if (this.hasId()) {
|
||||
// Verify authentication.
|
||||
return Api.get("session/" + this.getId())
|
||||
.then((resp) => {
|
||||
this.setResp(resp);
|
||||
|
@ -309,6 +319,7 @@ export default class Session {
|
|||
return Promise.reject();
|
||||
});
|
||||
} else {
|
||||
// No authentication yet.
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -521,10 +521,7 @@
|
|||
|
||||
<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="primary-button elevation-0 clickable"
|
||||
></v-img>
|
||||
<img :src="userAvatarURL" :alt="displayName">
|
||||
</v-list-tile-avatar>
|
||||
|
||||
<v-list-tile-content>
|
||||
|
@ -663,8 +660,6 @@
|
|||
<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",
|
||||
|
@ -746,6 +741,9 @@ export default {
|
|||
|
||||
return this.$gettext("Unregistered");
|
||||
},
|
||||
userAvatarURL() {
|
||||
return this.$session.getUser().getAvatarURL('tile_50');
|
||||
},
|
||||
accountInfo() {
|
||||
const user = this.$session.getUser();
|
||||
if (user) {
|
||||
|
|
|
@ -44,30 +44,10 @@ Additional information can be found in our Developer Guide:
|
|||
@import url("pages.css");
|
||||
@import url("help.css");
|
||||
@import url("auth.css");
|
||||
@import url("layout.css");
|
||||
@import url("rtl.css");
|
||||
|
||||
@media (min-width: 1750px) {
|
||||
.flex.xlg2 {
|
||||
flex-basis: 16.666666666666664%;
|
||||
flex-grow: 0;
|
||||
max-width: 16.666666666666664%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 2400px) {
|
||||
.flex.xxl1 {
|
||||
flex-basis: 8.333333333333332%;
|
||||
flex-grow: 0;
|
||||
max-width: 8.333333333333332%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 2550px) {
|
||||
.flex.xxxl1 {
|
||||
flex-basis: 8.333333333333332%;
|
||||
flex-grow: 0;
|
||||
max-width: 8.333333333333332%;
|
||||
}
|
||||
}
|
||||
/* Core Element Styles */
|
||||
|
||||
html,
|
||||
body {
|
||||
|
@ -82,51 +62,14 @@ body {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
clear: both;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
#photoprism .footer {
|
||||
margin: 0;
|
||||
padding: 6px 24px 12px;
|
||||
}
|
||||
|
||||
#photoprism .footer div > span,
|
||||
#photoprism .footer div > a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 599px) {
|
||||
#photoprism .footer div > span,
|
||||
#photoprism .footer div > a {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#photoprism .footer .text-link {
|
||||
float: left;
|
||||
}
|
||||
|
||||
#photoprism .footer .body-link {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
#photoprism .footer .footer-actions {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#photoprism .p-about-footer .body-1 {
|
||||
line-height: 1.8em;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Page Overlay */
|
||||
|
||||
#busy-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
|
@ -138,6 +81,8 @@ main {
|
|||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* General App Styles */
|
||||
|
||||
#photoprism div.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
@ -221,30 +166,13 @@ main {
|
|||
height: 1.1rem;
|
||||
}
|
||||
|
||||
/* Line Height */
|
||||
|
||||
.lh-15 {
|
||||
line-height: 1.5rem !important;
|
||||
ol, ul {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.lh-16 {
|
||||
line-height: 1.6rem !important;
|
||||
}
|
||||
|
||||
.lh-17 {
|
||||
line-height: 1.7rem !important;
|
||||
}
|
||||
|
||||
.lh-18 {
|
||||
line-height: 1.8rem !important;
|
||||
}
|
||||
|
||||
.lh-19 {
|
||||
line-height: 1.9rem !important;
|
||||
}
|
||||
|
||||
.lh-20 {
|
||||
line-height: 2.0rem !important;
|
||||
#photoprism .search-results.list-view .result {
|
||||
padding: 1px 0 1px 8px !important;
|
||||
}
|
||||
|
||||
/* Image Orientation */
|
||||
|
@ -284,139 +212,3 @@ main {
|
|||
.orientation-8 {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
|
||||
/* RTL alignments */
|
||||
|
||||
#photoprism.is-rtl .text-xs-left,
|
||||
#photoprism.is-rtl .text-sm-left {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
#photoprism.is-rtl .text-xs-right,
|
||||
#photoprism.is-rtl .text-sm-right {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
#photoprism.is-rtl .v-text-field .v-input__prepend-inner {
|
||||
margin-left: auto;
|
||||
padding-left: 4px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
#photoprism.is-rtl .v-toolbar.page-toolbar .v-text-field .v-input__slot {
|
||||
padding-left: 12px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
#photoprism.is-rtl .card-details button {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#photoprism.is-rtl .p-flex-menuitem .nav-count {
|
||||
margin-left: 48px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#photoprism.is-rtl .v-list__group__header .p-flex-menuitem .nav-count {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#photoprism.is-rtl .v-toolbar__content > :last-child.v-btn--icon,
|
||||
#photoprism.is-rtl .v-toolbar__extension > :last-child.v-btn--icon {
|
||||
margin-left: -6px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
/* Rounded Elements */
|
||||
|
||||
.v-progress-linear,
|
||||
.v-progress-linear .v-progress-linear__bar__determinate,
|
||||
.v-progress-linear .v-progress-linear__bar__indeterminate--active {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
|
||||
/* Result Border Radius */
|
||||
|
||||
.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,
|
||||
#photoprism .mosaic-view .result.image,
|
||||
#photoprism .list-view .result .image {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.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 > form > div.v-card,
|
||||
#photoprism div.v-dialog > form > div.v-card > .v-card__text .v-expansion-panel__container {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.ra-3,
|
||||
.ra-3 > a,
|
||||
.rounded-3,
|
||||
.rounded-3 > a {
|
||||
border-radius: 3px !important;
|
||||
}
|
||||
|
||||
.ra-4,
|
||||
.ra-4 > a,
|
||||
.rounded-4,
|
||||
.rounded-4 > a {
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.ra-5,
|
||||
.ra-5 > a,
|
||||
.rounded-5,
|
||||
.rounded-5 > a {
|
||||
border-radius: 5px !important;
|
||||
}
|
||||
|
||||
.ra-6,
|
||||
.ra-6 > a,
|
||||
.rounded-6,
|
||||
.rounded-6 > a {
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
|
||||
.ra-8,
|
||||
.ra-8 > a,
|
||||
.rounded-8,
|
||||
.rounded-8 > a {
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.ra-10,
|
||||
.ra-10 > a,
|
||||
.rounded-10,
|
||||
.rounded-10 > a {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
#photoprism div.v-dialog.v-dialog--fullscreen > div.v-card {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.v-autocomplete__content.v-menu__content,
|
||||
.v-autocomplete__content.v-menu__content .v-card {
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.v-snack.v-snack--bottom .v-snack__wrapper {
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
#photoprism .search-results.list-view .result {
|
||||
padding: 1px 0 1px 8px !important;
|
||||
}
|
||||
|
|
|
@ -88,8 +88,9 @@
|
|||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
max-width: 150px;
|
||||
max-height: 150px;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
overflow: hidden;
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
/* Font Settings */
|
||||
|
||||
body {
|
||||
color: #333333;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
|
@ -26,4 +28,30 @@ body .application {
|
|||
button.v-btn.compact {
|
||||
font-size: 14px;
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Line Height */
|
||||
|
||||
.lh-15 {
|
||||
line-height: 1.5rem !important;
|
||||
}
|
||||
|
||||
.lh-16 {
|
||||
line-height: 1.6rem !important;
|
||||
}
|
||||
|
||||
.lh-17 {
|
||||
line-height: 1.7rem !important;
|
||||
}
|
||||
|
||||
.lh-18 {
|
||||
line-height: 1.8rem !important;
|
||||
}
|
||||
|
||||
.lh-19 {
|
||||
line-height: 1.9rem !important;
|
||||
}
|
||||
|
||||
.lh-20 {
|
||||
line-height: 2.0rem !important;
|
||||
}
|
||||
|
|
168
frontend/src/css/layout.css
Normal file
168
frontend/src/css/layout.css
Normal file
|
@ -0,0 +1,168 @@
|
|||
/* Layout Breakpoints */
|
||||
|
||||
@media (min-width: 1750px) {
|
||||
.flex.xlg2 {
|
||||
flex-basis: 16.666666666666664%;
|
||||
flex-grow: 0;
|
||||
max-width: 16.666666666666664%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 2400px) {
|
||||
.flex.xxl1 {
|
||||
flex-basis: 8.333333333333332%;
|
||||
flex-grow: 0;
|
||||
max-width: 8.333333333333332%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 2550px) {
|
||||
.flex.xxxl1 {
|
||||
flex-basis: 8.333333333333332%;
|
||||
flex-grow: 0;
|
||||
max-width: 8.333333333333332%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Layout Widths */
|
||||
|
||||
|
||||
.width-sm, .width-md, .width-lg {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.width-sm {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.width-md {
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.width-lg {
|
||||
max-width: 1264px;
|
||||
}
|
||||
|
||||
/* Rounded Elements */
|
||||
|
||||
.v-progress-linear,
|
||||
.v-progress-linear .v-progress-linear__bar__determinate,
|
||||
.v-progress-linear .v-progress-linear__bar__indeterminate--active {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.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,
|
||||
#photoprism .mosaic-view .result.image,
|
||||
#photoprism .list-view .result .image {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.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 > form > div.v-card,
|
||||
#photoprism div.v-dialog > form > div.v-card > .v-card__text .v-expansion-panel__container {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.ra-3,
|
||||
.ra-3 > a,
|
||||
.rounded-3,
|
||||
.rounded-3 > a {
|
||||
border-radius: 3px !important;
|
||||
}
|
||||
|
||||
.ra-4,
|
||||
.ra-4 > a,
|
||||
.rounded-4,
|
||||
.rounded-4 > a {
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.ra-5,
|
||||
.ra-5 > a,
|
||||
.rounded-5,
|
||||
.rounded-5 > a {
|
||||
border-radius: 5px !important;
|
||||
}
|
||||
|
||||
.ra-6,
|
||||
.ra-6 > a,
|
||||
.rounded-6,
|
||||
.rounded-6 > a {
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
|
||||
.ra-8,
|
||||
.ra-8 > a,
|
||||
.rounded-8,
|
||||
.rounded-8 > a {
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.ra-10,
|
||||
.ra-10 > a,
|
||||
.rounded-10,
|
||||
.rounded-10 > a {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
#photoprism div.v-dialog.v-dialog--fullscreen > div.v-card {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.v-autocomplete__content.v-menu__content,
|
||||
.v-autocomplete__content.v-menu__content .v-card {
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.v-snack.v-snack--bottom .v-snack__wrapper {
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
/* Page Footer */
|
||||
|
||||
footer {
|
||||
clear: both;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
#photoprism .footer {
|
||||
margin: 0;
|
||||
padding: 6px 24px 12px;
|
||||
}
|
||||
|
||||
#photoprism .footer div > span,
|
||||
#photoprism .footer div > a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 599px) {
|
||||
#photoprism .footer div > span,
|
||||
#photoprism .footer div > a {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#photoprism .footer .text-link {
|
||||
float: left;
|
||||
}
|
||||
|
||||
#photoprism .footer .body-link {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
#photoprism .footer .footer-actions {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#photoprism .p-about-footer .body-1 {
|
||||
line-height: 1.8em;
|
||||
}
|
41
frontend/src/css/rtl.css
Normal file
41
frontend/src/css/rtl.css
Normal file
|
@ -0,0 +1,41 @@
|
|||
/* Right-to-left (RTL) Language Styles */
|
||||
|
||||
#photoprism.is-rtl .text-xs-left,
|
||||
#photoprism.is-rtl .text-sm-left {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
#photoprism.is-rtl .text-xs-right,
|
||||
#photoprism.is-rtl .text-sm-right {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
#photoprism.is-rtl .v-text-field .v-input__prepend-inner {
|
||||
margin-left: auto;
|
||||
padding-left: 4px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
#photoprism.is-rtl .v-toolbar.page-toolbar .v-text-field .v-input__slot {
|
||||
padding-left: 12px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
#photoprism.is-rtl .card-details button {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#photoprism.is-rtl .p-flex-menuitem .nav-count {
|
||||
margin-left: 48px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#photoprism.is-rtl .v-list__group__header .p-flex-menuitem .nav-count {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#photoprism.is-rtl .v-toolbar__content > :last-child.v-btn--icon,
|
||||
#photoprism.is-rtl .v-toolbar__extension > :last-child.v-btn--icon {
|
||||
margin-left: -6px;
|
||||
margin-right: 6px;
|
||||
}
|
|
@ -146,7 +146,7 @@ export default {
|
|||
},
|
||||
cancel() {
|
||||
if (this.busy) {
|
||||
Notify.info(this.$gettext("Uploading…"));
|
||||
Notify.info(this.$gettext("Uploading photos…"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -154,7 +154,7 @@ export default {
|
|||
},
|
||||
confirm() {
|
||||
if (this.busy) {
|
||||
Notify.info(this.$gettext("Uploading…"));
|
||||
Notify.info(this.$gettext("Uploading photos…"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -201,7 +201,7 @@ export default {
|
|||
|
||||
let userUid = this.$session.getUserUID();
|
||||
|
||||
Notify.info(this.$gettext("Uploading…"));
|
||||
Notify.info(this.$gettext("Uploading photos…"));
|
||||
|
||||
let addToAlbums = [];
|
||||
|
||||
|
|
|
@ -31,10 +31,10 @@ msgstr ""
|
|||
msgid "%{n} people found"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/album/photos.vue:222
|
||||
#: src/page/album/photos.vue:375
|
||||
#: src/page/photos.vue:308
|
||||
#: src/page/photos.vue:474
|
||||
#: src/page/album/photos.vue:240
|
||||
#: src/page/album/photos.vue:393
|
||||
#: src/page/photos.vue:325
|
||||
#: src/page/photos.vue:490
|
||||
msgid "%{n} pictures found"
|
||||
msgstr ""
|
||||
|
||||
|
@ -67,11 +67,11 @@ msgid "Abyss"
|
|||
msgstr ""
|
||||
|
||||
#: src/component/navigation.vue:3
|
||||
#: src/component/navigation.vue:92
|
||||
#: src/component/navigation.vue:1909
|
||||
#: src/component/navigation.vue:93
|
||||
#: src/component/navigation.vue:1907
|
||||
#: src/dialog/share/upload.vue:108
|
||||
#: src/model/service.js:98
|
||||
#: src/model/user.js:142
|
||||
#: src/model/user.js:144
|
||||
#: src/page/settings.vue:83
|
||||
#: src/page/settings/general.vue:470
|
||||
msgid "Account"
|
||||
|
@ -125,10 +125,6 @@ msgstr ""
|
|||
msgid "Added"
|
||||
msgstr ""
|
||||
|
||||
#: src/options/options.js:415
|
||||
msgid "Admin"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings.vue:59
|
||||
msgid "Advanced"
|
||||
msgstr ""
|
||||
|
@ -261,7 +257,7 @@ msgstr ""
|
|||
msgid "Altitude (m)"
|
||||
msgstr ""
|
||||
|
||||
#: src/common/api.js:104
|
||||
#: src/common/api.js:102
|
||||
msgid "An error occurred - are you offline?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -370,10 +366,14 @@ msgid "Being 100% self-funded and independent, we can promise you that we will n
|
|||
msgstr ""
|
||||
|
||||
#: src/page/people/recognized.vue:434
|
||||
#: src/page/settings/account.vue:607
|
||||
#: src/page/settings/account.vue:279
|
||||
msgid "Bio"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:67
|
||||
msgid "Birth Date"
|
||||
msgstr ""
|
||||
|
||||
#: src/options/options.js:395
|
||||
msgid "Black"
|
||||
msgstr ""
|
||||
|
@ -428,8 +428,8 @@ msgstr ""
|
|||
msgid "Camera Serial"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/album/photos.vue:228
|
||||
#: src/page/photos.vue:314
|
||||
#: src/page/album/photos.vue:246
|
||||
#: src/page/photos.vue:331
|
||||
msgid "Can't load more, limit reached"
|
||||
msgstr ""
|
||||
|
||||
|
@ -480,7 +480,7 @@ msgid "Category"
|
|||
msgstr ""
|
||||
|
||||
#: src/dialog/account/password.vue:7
|
||||
#: src/page/settings/account.vue:38
|
||||
#: src/page/settings/account.vue:51
|
||||
msgid "Change Password"
|
||||
msgstr ""
|
||||
|
||||
|
@ -496,6 +496,14 @@ msgstr ""
|
|||
msgid "Change private flag"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:106
|
||||
#: src/page/settings/account.vue:121
|
||||
#: src/page/settings/advanced.vue:42
|
||||
#: src/page/settings/general.vue:105
|
||||
#: src/page/settings/library.vue:50
|
||||
msgid "Changes successfully saved"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/edit/info.vue:191
|
||||
msgid "Checked"
|
||||
msgstr ""
|
||||
|
@ -549,7 +557,7 @@ msgid "Connect"
|
|||
msgstr ""
|
||||
|
||||
#: src/dialog/webdav.vue:4
|
||||
#: src/page/settings/account.vue:44
|
||||
#: src/page/settings/account.vue:57
|
||||
#: src/page/settings/services.vue:48
|
||||
msgid "Connect via WebDAV"
|
||||
msgstr ""
|
||||
|
@ -558,7 +566,7 @@ msgstr ""
|
|||
msgid "Connected"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:78
|
||||
#: src/page/settings/account.vue:90
|
||||
msgid "Contact Details"
|
||||
msgstr ""
|
||||
|
||||
|
@ -578,10 +586,6 @@ msgstr ""
|
|||
msgid "Contains one picture."
|
||||
msgstr ""
|
||||
|
||||
#: src/options/options.js:420
|
||||
msgid "Contributor"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/library.vue:111
|
||||
msgid "Convert to JPEG"
|
||||
msgstr ""
|
||||
|
@ -605,7 +609,7 @@ msgstr ""
|
|||
|
||||
#: src/component/photo/toolbar.vue:178
|
||||
#: src/dialog/photo/edit/details.vue:248
|
||||
#: src/page/settings/account.vue:495
|
||||
#: src/page/settings/account.vue:578
|
||||
msgid "Country"
|
||||
msgstr ""
|
||||
|
||||
|
@ -646,6 +650,7 @@ msgid "Daily"
|
|||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/edit/details.vue:116
|
||||
#: src/page/settings/account.vue:444
|
||||
msgid "Day"
|
||||
msgstr ""
|
||||
|
||||
|
@ -746,7 +751,7 @@ msgstr ""
|
|||
msgid "Discover"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:134
|
||||
#: src/page/settings/account.vue:191
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
|
@ -885,8 +890,7 @@ msgstr ""
|
|||
msgid "Edited"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:159
|
||||
#: src/page/settings/account.vue:415
|
||||
#: src/page/settings/account.vue:219
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
|
@ -970,8 +974,8 @@ msgstr ""
|
|||
msgid "Failed updating link"
|
||||
msgstr ""
|
||||
|
||||
#: src/options/options.js:417
|
||||
msgid "Family"
|
||||
#: src/page/settings/account.vue:164
|
||||
msgid "Family Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/options/options.js:249
|
||||
|
@ -992,7 +996,7 @@ msgstr ""
|
|||
msgid "Feature Request"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:580
|
||||
#: src/page/settings/account.vue:669
|
||||
msgid "Feed"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1005,6 +1009,10 @@ msgstr ""
|
|||
msgid "Feel free to contact us at hello@photoprism.app if you have any questions."
|
||||
msgstr ""
|
||||
|
||||
#: src/options/options.js:416
|
||||
msgid "Female"
|
||||
msgstr ""
|
||||
|
||||
#: src/model/file.js:291
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
@ -1070,10 +1078,6 @@ msgstr ""
|
|||
msgid "Frames"
|
||||
msgstr ""
|
||||
|
||||
#: src/options/options.js:418
|
||||
msgid "Friend"
|
||||
msgstr ""
|
||||
|
||||
#: src/component/photo-viewer.vue:172
|
||||
msgid "Fullscreen"
|
||||
msgstr ""
|
||||
|
@ -1082,6 +1086,10 @@ msgstr ""
|
|||
msgid "Gemstone"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:80
|
||||
msgid "Gender"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings.vue:35
|
||||
msgid "General"
|
||||
msgstr ""
|
||||
|
@ -1090,6 +1098,10 @@ msgstr ""
|
|||
msgid "Getting Support"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:138
|
||||
msgid "Given Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/options/options.js:385
|
||||
msgid "Gold"
|
||||
msgstr ""
|
||||
|
@ -1112,10 +1124,6 @@ msgstr ""
|
|||
msgid "Group by similarity"
|
||||
msgstr ""
|
||||
|
||||
#: src/options/options.js:421
|
||||
msgid "Guest"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/edit/files.vue:59
|
||||
#: src/dialog/photo/edit/files.vue:56
|
||||
msgid "Hash"
|
||||
|
@ -1266,6 +1274,20 @@ msgstr ""
|
|||
msgid "Interval"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:89
|
||||
#: src/page/settings/account.vue:115
|
||||
#: src/page/settings/account.vue:141
|
||||
#: src/page/settings/account.vue:167
|
||||
#: src/page/settings/account.vue:222
|
||||
#: src/page/settings/account.vue:278
|
||||
#: src/page/settings/account.vue:562
|
||||
#: src/page/settings/account.vue:589
|
||||
#: src/page/settings/account.vue:616
|
||||
#: src/page/settings/account.vue:644
|
||||
#: src/page/settings/account.vue:672
|
||||
msgid "Invalid"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/edit/details.vue:112
|
||||
msgid "Invalid date"
|
||||
msgstr ""
|
||||
|
@ -1427,7 +1449,7 @@ msgstr ""
|
|||
#: src/component/photo/cards.vue:512
|
||||
#: src/component/photo/list.vue:138
|
||||
#: src/dialog/album/edit.vue:125
|
||||
#: src/page/settings/account.vue:477
|
||||
#: src/page/settings/account.vue:559
|
||||
msgid "Location"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1438,14 +1460,14 @@ msgstr ""
|
|||
|
||||
#: src/component/navigation.vue:454
|
||||
#: src/component/navigation.vue:1686
|
||||
#: src/component/navigation.vue:1961
|
||||
#: src/component/navigation.vue:1959
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
|
||||
#: src/component/navigation.vue:501
|
||||
#: src/component/navigation.vue:1798
|
||||
#: src/component/navigation.vue:1829
|
||||
#: src/component/navigation.vue:1880
|
||||
#: src/component/navigation.vue:1796
|
||||
#: src/component/navigation.vue:1827
|
||||
#: src/component/navigation.vue:1878
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1473,6 +1495,10 @@ msgstr ""
|
|||
msgid "Main Color"
|
||||
msgstr ""
|
||||
|
||||
#: src/options/options.js:415
|
||||
msgid "Male"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/edit/labels.vue:35
|
||||
msgid "manual"
|
||||
msgstr ""
|
||||
|
@ -1519,6 +1545,7 @@ msgstr ""
|
|||
|
||||
#: src/component/photo/toolbar.vue:291
|
||||
#: src/dialog/photo/edit/details.vue:142
|
||||
#: src/page/settings/account.vue:467
|
||||
msgid "Month"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1526,8 +1553,8 @@ msgstr ""
|
|||
msgid "Moonlight"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/album/photos.vue:378
|
||||
#: src/page/photos.vue:477
|
||||
#: src/page/album/photos.vue:396
|
||||
#: src/page/photos.vue:493
|
||||
msgid "More than %{n} pictures found"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1650,9 +1677,9 @@ msgstr ""
|
|||
#: src/component/photo/list.vue:8
|
||||
#: src/component/photo/mosaic.vue:8
|
||||
#: src/component/photo/mosaic.vue:6
|
||||
#: src/page/album/photos.vue:371
|
||||
#: src/page/album/photos.vue:389
|
||||
#: src/page/library/browse.vue:34
|
||||
#: src/page/photos.vue:470
|
||||
#: src/page/photos.vue:486
|
||||
#: src/page/places.vue:217
|
||||
#: src/page/places.vue:279
|
||||
msgid "No pictures found"
|
||||
|
@ -1780,8 +1807,8 @@ msgstr ""
|
|||
msgid "One person found"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/album/photos.vue:373
|
||||
#: src/page/photos.vue:472
|
||||
#: src/page/album/photos.vue:391
|
||||
#: src/page/photos.vue:488
|
||||
msgid "One picture found"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1801,10 +1828,6 @@ msgstr ""
|
|||
msgid "Orange"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:340
|
||||
msgid "Organization"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/edit/files.vue:155
|
||||
#: src/dialog/photo/edit/files.vue:152
|
||||
msgid "Orientation"
|
||||
|
@ -1829,6 +1852,7 @@ msgid "Originals"
|
|||
msgstr ""
|
||||
|
||||
#: src/options/options.js:404
|
||||
#: src/options/options.js:417
|
||||
msgid "Other"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1884,7 +1908,7 @@ msgstr ""
|
|||
msgid "Permanently remove files to free up storage."
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:528
|
||||
#: src/page/settings/account.vue:613
|
||||
msgid "Phone"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1995,10 +2019,6 @@ msgstr ""
|
|||
msgid "Product Feedback"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:554
|
||||
msgid "Profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/edit/files.vue:143
|
||||
#: src/dialog/photo/edit/files.vue:140
|
||||
msgid "Projection"
|
||||
|
@ -2080,7 +2100,7 @@ msgid "Red"
|
|||
msgstr ""
|
||||
|
||||
#: src/component/album/toolbar.vue:64
|
||||
#: src/component/navigation.vue:1894
|
||||
#: src/component/navigation.vue:1892
|
||||
#: src/component/photo/toolbar.vue:54
|
||||
#: src/dialog/reload.vue:15
|
||||
#: src/page/albums.vue:126
|
||||
|
@ -2092,7 +2112,7 @@ msgstr ""
|
|||
msgid "Reload"
|
||||
msgstr ""
|
||||
|
||||
#: src/component/navigation.vue:123
|
||||
#: src/component/navigation.vue:124
|
||||
#: src/dialog/reload.vue:26
|
||||
#: src/page/settings/general.vue:101
|
||||
#: src/page/settings/library.vue:46
|
||||
|
@ -2127,6 +2147,7 @@ msgstr ""
|
|||
#: src/page/about/feedback.vue:130
|
||||
#: src/page/about/feedback.vue:150
|
||||
#: src/page/about/feedback.vue:192
|
||||
#: src/page/settings/account.vue:194
|
||||
msgid "Required"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2196,7 +2217,7 @@ msgstr ""
|
|||
msgid "Secret"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:31
|
||||
#: src/page/settings/account.vue:44
|
||||
msgid "Security and Access"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2250,18 +2271,10 @@ msgstr ""
|
|||
#: src/component/navigation.vue:18
|
||||
#: src/component/navigation.vue:4
|
||||
#: src/component/navigation.vue:1520
|
||||
#: src/component/navigation.vue:1927
|
||||
#: src/component/navigation.vue:1925
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:68
|
||||
#: src/page/settings/account.vue:83
|
||||
#: src/page/settings/advanced.vue:42
|
||||
#: src/page/settings/general.vue:105
|
||||
#: src/page/settings/library.vue:50
|
||||
msgid "Settings saved"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/share/upload.vue:32
|
||||
msgid "Setup"
|
||||
msgstr ""
|
||||
|
@ -2504,7 +2517,7 @@ msgstr ""
|
|||
#: src/component/photo/list.vue:135
|
||||
#: src/dialog/photo/edit/details.vue:95
|
||||
#: src/dialog/photo/edit/info.vue:45
|
||||
#: src/page/settings/account.vue:389
|
||||
#: src/page/settings/account.vue:112
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2553,10 +2566,6 @@ msgstr ""
|
|||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#: src/options/options.js:423
|
||||
msgid "Unauthorized"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/edit/people.vue:146
|
||||
msgid "Undo"
|
||||
msgstr ""
|
||||
|
@ -2579,16 +2588,15 @@ msgstr ""
|
|||
#: src/options/options.js:51
|
||||
#: src/options/options.js:65
|
||||
#: src/options/options.js:77
|
||||
#: src/options/options.js:424
|
||||
#: src/page/library/errors.vue:168
|
||||
#: src/page/library/errors.vue:175
|
||||
#: src/page/library/logs.vue:18
|
||||
msgid "Unknown"
|
||||
msgstr ""
|
||||
|
||||
#: src/component/navigation.vue:84
|
||||
#: src/model/user.js:128
|
||||
#: src/page/settings/account.vue:38
|
||||
#: src/component/navigation.vue:82
|
||||
#: src/model/user.js:130
|
||||
#: src/page/settings/account.vue:35
|
||||
msgid "Unregistered"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2620,6 +2628,10 @@ msgstr ""
|
|||
msgid "Updating moments"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:116
|
||||
msgid "Updating picture…"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/library/index.vue:153
|
||||
msgid "Updating previews"
|
||||
msgstr ""
|
||||
|
@ -2629,7 +2641,7 @@ msgid "Updating stacks"
|
|||
msgstr ""
|
||||
|
||||
#: src/component/album/toolbar.vue:192
|
||||
#: src/component/navigation.vue:1945
|
||||
#: src/component/navigation.vue:1943
|
||||
#: src/component/photo/toolbar.vue:122
|
||||
#: src/dialog/share/upload.vue:35
|
||||
#: src/dialog/upload.vue:8
|
||||
|
@ -2669,8 +2681,7 @@ msgstr ""
|
|||
#: src/dialog/upload.vue:60
|
||||
#: src/dialog/upload.vue:68
|
||||
#: src/dialog/upload.vue:115
|
||||
#: src/page/settings/account.vue:78
|
||||
msgid "Uploading…"
|
||||
msgid "Uploading photos…"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/upload.vue:46
|
||||
|
@ -2678,7 +2689,6 @@ msgid "Uploads that may contain such images will be rejected automatically."
|
|||
msgstr ""
|
||||
|
||||
#: src/dialog/share.vue:153
|
||||
#: src/page/settings/account.vue:365
|
||||
msgid "URL"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2686,8 +2696,7 @@ msgstr ""
|
|||
msgid "Use Presets"
|
||||
msgstr ""
|
||||
|
||||
#: src/model/user.js:209
|
||||
#: src/options/options.js:416
|
||||
#: src/model/user.js:211
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2703,7 +2712,6 @@ msgstr ""
|
|||
#: src/dialog/service/add.vue:103
|
||||
#: src/dialog/service/edit.vue:467
|
||||
#: src/dialog/share.vue:23
|
||||
#: src/page/settings/account.vue:110
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2740,18 +2748,10 @@ msgstr ""
|
|||
msgid "View"
|
||||
msgstr ""
|
||||
|
||||
#: src/options/options.js:419
|
||||
msgid "Viewer"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/about/about.vue:47
|
||||
msgid "Visit docs.photoprism.app/user-guide to learn how to sync, organize, and share your pictures."
|
||||
msgstr ""
|
||||
|
||||
#: src/options/options.js:422
|
||||
msgid "Visitor"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/about/feedback.vue:13
|
||||
msgid "We appreciate your feedback!"
|
||||
msgstr ""
|
||||
|
@ -2783,16 +2783,17 @@ msgstr ""
|
|||
msgid "WebDAV Upload"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:641
|
||||
msgid "Website"
|
||||
msgstr ""
|
||||
|
||||
#: src/options/options.js:393
|
||||
msgid "White"
|
||||
msgstr ""
|
||||
|
||||
#: src/page/settings/account.vue:54
|
||||
msgid "Work Details"
|
||||
msgstr ""
|
||||
|
||||
#: src/component/photo/toolbar.vue:268
|
||||
#: src/dialog/photo/edit/details.vue:168
|
||||
#: src/page/settings/account.vue:490
|
||||
msgid "Year"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -67,7 +67,6 @@ export class User extends RestModel {
|
|||
UpdatedAt: "",
|
||||
},
|
||||
Details: {
|
||||
IdURL: "",
|
||||
SubjUID: "",
|
||||
SubjSrc: "",
|
||||
PlaceID: "",
|
||||
|
@ -76,13 +75,15 @@ export class User extends RestModel {
|
|||
BirthYear: -1,
|
||||
BirthMonth: -1,
|
||||
BirthDay: -1,
|
||||
NamePrefix: "",
|
||||
NameTitle: "",
|
||||
GivenName: "",
|
||||
MiddleName: "",
|
||||
FamilyName: "",
|
||||
NameSuffix: "",
|
||||
NickName: "",
|
||||
NameSrc: "",
|
||||
Gender: "",
|
||||
About: "",
|
||||
Bio: "",
|
||||
Location: "",
|
||||
Country: "",
|
||||
|
@ -91,11 +92,12 @@ export class User extends RestModel {
|
|||
ProfileURL: "",
|
||||
FeedURL: "",
|
||||
AvatarURL: "",
|
||||
OrgName: "",
|
||||
OrgTitle: "",
|
||||
OrgName: "",
|
||||
OrgEmail: "",
|
||||
OrgPhone: "",
|
||||
OrgURL: "",
|
||||
IdURL: "",
|
||||
CreatedAt: "",
|
||||
UpdatedAt: "",
|
||||
},
|
||||
|
@ -153,22 +155,22 @@ 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}`;
|
||||
if (this.Thumb) {
|
||||
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken}/${size}`;
|
||||
} else {
|
||||
return `${config.staticUri}/img/avatar/${size}.jpg`;
|
||||
}
|
||||
}
|
||||
|
||||
uploadAvatar(files) {
|
||||
if (this.busy) {
|
||||
return;
|
||||
return Promise.reject(this);
|
||||
} else if (!files || files.length !== 1) {
|
||||
return;
|
||||
return Promise.reject(this);
|
||||
}
|
||||
|
||||
let file = files[0];
|
||||
|
|
|
@ -411,15 +411,8 @@ export const ThumbFilters = () => [
|
|||
{ value: "linear", text: $gettext("Linear: Very Smooth, Best Performance") },
|
||||
];
|
||||
|
||||
export const UserRoles = () => [
|
||||
{ value: "admin", text: $gettext("Admin") },
|
||||
{ value: "user", text: $gettext("User") },
|
||||
{ value: "family", text: $gettext("Family") },
|
||||
{ value: "friend", text: $gettext("Friend") },
|
||||
{ value: "viewer", text: $gettext("Viewer") },
|
||||
{ value: "contributor", text: $gettext("Contributor") },
|
||||
{ value: "guest", text: $gettext("Guest") },
|
||||
{ value: "visitor", text: $gettext("Visitor") },
|
||||
{ value: "unauthorized", text: $gettext("Unauthorized") },
|
||||
{ value: "", text: $gettext("Unknown") },
|
||||
export const Gender = () => [
|
||||
{ value: "male", text: $gettext("Male") },
|
||||
{ value: "female", text: $gettext("Female") },
|
||||
{ value: "other", text: $gettext("Other") },
|
||||
];
|
||||
|
|
|
@ -1,56 +1,96 @@
|
|||
<template>
|
||||
<div class="p-tab p-settings-account">
|
||||
<v-form ref="form" lazy-validation
|
||||
dense class="p-form-account" accept-charset="UTF-8"
|
||||
<v-form ref="form" v-model="valid" lazy-validation dense class="p-form-account pb-4 width-lg" 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="primary-button elevation-0 clickable"
|
||||
></v-img>
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex xs8 sm9 md10 xl11 fill-height class="pa-0">
|
||||
<v-flex xs8 sm9 md10 fill-height class="pa-0">
|
||||
<v-layout wrap align-top>
|
||||
<v-flex xs12 md3 class="pa-2">
|
||||
<v-flex md2 class="pa-2 hidden-sm-and-down">
|
||||
<v-select v-model="user.Details.Gender"
|
||||
:label="$gettext('Gender')"
|
||||
hide-details box flat
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
color="secondary-dark"
|
||||
:items="options.Gender()"
|
||||
class="input-gender"
|
||||
:rules="[v => validLength(v, 0, 16) || $gettext('Invalid')]"
|
||||
@change="onChange">
|
||||
</v-select>
|
||||
</v-flex>
|
||||
<v-flex md2 class="pa-2 hidden-sm-and-down">
|
||||
<v-text-field
|
||||
v-model="user.Name"
|
||||
hide-details required box flat readonly
|
||||
v-model="user.Details.NameTitle"
|
||||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
maxlength="32"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Username')"
|
||||
class="input-name"
|
||||
:label="$gettext('Title')"
|
||||
class="input-name-title"
|
||||
color="secondary-dark"
|
||||
:rules="[v => validLength(v, 0, 32) || $gettext('Invalid')]"
|
||||
@change="onChangeName"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12 md9 class="pa-2">
|
||||
<v-flex md4 class="pa-2 hidden-sm-and-down">
|
||||
<v-text-field
|
||||
v-model="user.Details.GivenName"
|
||||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
maxlength="64"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Given Name')"
|
||||
class="input-given-name"
|
||||
color="secondary-dark"
|
||||
:rules="[v => validLength(v, 0, 64) || $gettext('Invalid')]"
|
||||
@change="onChangeName"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex md4 class="pa-2 hidden-sm-and-down">
|
||||
<v-text-field
|
||||
v-model="user.Details.FamilyName"
|
||||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
maxlength="64"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Family Name')"
|
||||
class="input-family-name"
|
||||
color="secondary-dark"
|
||||
:rules="[v => validLength(v, 0, 64) || $gettext('Invalid')]"
|
||||
@change="onChangeName"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12 md4 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="user.DisplayName"
|
||||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
maxlength="200"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Display Name')"
|
||||
class="input-display-name"
|
||||
color="secondary-dark"
|
||||
:rules="[v => validLength(v, 1, 200) || $gettext('Required')]"
|
||||
@change="onChange"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12 class="pa-2">
|
||||
<v-flex xs12 md8 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="user.Email"
|
||||
hide-details required box flat
|
||||
type="email"
|
||||
maxlength="250"
|
||||
:disabled="busy"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
|
@ -58,11 +98,30 @@
|
|||
:label="$gettext('Email')"
|
||||
class="input-email"
|
||||
color="secondary-dark"
|
||||
:rules="[v => !!v && validEmail(v) || $gettext('Invalid')]"
|
||||
@change="onChange"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-flex>
|
||||
<v-flex
|
||||
class="pa-2 text-xs-center"
|
||||
xs4 sm3 md2 align-self-center
|
||||
>
|
||||
<v-avatar :size="$vuetify.breakpoint.xsOnly ? 100 : 128" :class="{'clickable': !busy}" @click.stop.prevent="onChangeAvatar()">
|
||||
<img :src="$vuetify.breakpoint.xsOnly ? user.getAvatarURL('tile_100') : user.getAvatarURL('tile_224')" :alt="displayName">
|
||||
</v-avatar>
|
||||
</v-flex>
|
||||
<v-flex xs12 class="pa-2">
|
||||
<v-textarea v-model="user.Details.Bio" auto-grow flat box hide-details
|
||||
rows="2" class="input-bio" color="secondary-dark"
|
||||
autocorrect="off" autocapitalize="none" browser-autocomplete="off"
|
||||
:disabled="busy"
|
||||
maxlength="500"
|
||||
:rules="[v => validLength(v, 0, 500) || $gettext('Invalid')]"
|
||||
:label="$gettext('Bio')"
|
||||
@change="onChange"></v-textarea>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
@ -94,69 +153,49 @@
|
|||
<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>
|
||||
<translate>Birth Date</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
|
||||
<v-flex xs3 class="pa-2">
|
||||
<v-autocomplete
|
||||
v-model="user.Details.BirthDay"
|
||||
:disabled="busy"
|
||||
:label="$gettext('Day')"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Organization')"
|
||||
class="input-org-name"
|
||||
hide-no-data hide-details box flat
|
||||
color="secondary-dark"
|
||||
@change="onChange"
|
||||
></v-text-field>
|
||||
:items="options.Days()"
|
||||
class="input-birth-day"
|
||||
@change="onChange">
|
||||
</v-autocomplete>
|
||||
</v-flex>
|
||||
<v-flex xs3 class="pa-2">
|
||||
<v-autocomplete
|
||||
v-model="user.Details.BirthMonth"
|
||||
:disabled="busy"
|
||||
:label="$gettext('Month')"
|
||||
browser-autocomplete="off"
|
||||
hide-no-data hide-details box flat
|
||||
color="secondary-dark"
|
||||
:items="options.MonthsShort()"
|
||||
class="input-birth-month"
|
||||
@change="onChange">
|
||||
</v-autocomplete>
|
||||
</v-flex>
|
||||
<v-flex xs6 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="user.Details.OrgURL"
|
||||
hide-details required box flat
|
||||
<v-autocomplete
|
||||
v-model="user.Details.BirthYear"
|
||||
:disabled="busy"
|
||||
type="url"
|
||||
:label="$gettext('Year')"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('URL')"
|
||||
class="input-org-url"
|
||||
hide-no-data hide-details box flat
|
||||
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>
|
||||
:items="options.Years()"
|
||||
class="input-birth-year"
|
||||
@change="onChange">
|
||||
</v-autocomplete>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card-actions>
|
||||
|
@ -174,57 +213,64 @@
|
|||
v-model="user.Details.Location"
|
||||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
maxlength="500"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Location')"
|
||||
class="input-location"
|
||||
color="secondary-dark"
|
||||
:rules="[v => validLength(v, 0, 500) || $gettext('Invalid')]"
|
||||
@change="onChange"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-flex xs12 sm4 class="pa-2">
|
||||
<v-autocomplete
|
||||
v-model="user.Details.Country"
|
||||
:disabled="busy"
|
||||
:label="$gettext('Country')" hide-details box flat
|
||||
hide-no-data
|
||||
:label="$gettext('Country')"
|
||||
hide-no-data hide-details box flat
|
||||
browser-autocomplete="off"
|
||||
color="secondary-dark"
|
||||
item-value="Code"
|
||||
item-text="Name"
|
||||
:items="countries"
|
||||
class="input-country"
|
||||
:rules="[v => validLength(v, 0, 2) || $gettext('Invalid')]"
|
||||
@change="onChange"
|
||||
>
|
||||
</v-autocomplete>
|
||||
</v-flex>
|
||||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-flex xs12 sm8 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="user.Details.Phone"
|
||||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
maxlength="32"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Phone')"
|
||||
class="input-phone"
|
||||
color="secondary-dark"
|
||||
:rules="[v => validLength(v, 0, 32) || $gettext('Invalid')]"
|
||||
@change="onChange"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-text-field
|
||||
v-model="user.Details.ProfileURL"
|
||||
v-model="user.Details.SiteURL"
|
||||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
type="url"
|
||||
maxlength="500"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Profile')"
|
||||
class="input-profile-url"
|
||||
:label="$gettext('Website')"
|
||||
class="input-site-url"
|
||||
color="secondary-dark"
|
||||
:rules="[v => validUrl(v) || $gettext('Invalid')]"
|
||||
@change="onChange"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
|
@ -234,28 +280,21 @@
|
|||
hide-details required box flat
|
||||
:disabled="busy"
|
||||
type="url"
|
||||
maxlength="500"
|
||||
browser-autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Feed')"
|
||||
class="input-feed-url"
|
||||
color="secondary-dark"
|
||||
:rules="[v => validUrl(v) || $gettext('Invalid')]"
|
||||
@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>
|
||||
|
@ -265,8 +304,8 @@
|
|||
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";
|
||||
import * as options from "options/options";
|
||||
|
||||
export default {
|
||||
name: 'PSettingsAccount',
|
||||
|
@ -276,8 +315,10 @@ export default {
|
|||
const isPublic = this.$config.isPublic();
|
||||
return {
|
||||
busy: isDemo || isPublic,
|
||||
isDemo: isDemo,
|
||||
isPublic: isPublic,
|
||||
options,
|
||||
isDemo,
|
||||
isPublic,
|
||||
valid: true,
|
||||
rtl: this.$rtl,
|
||||
user: new User(this.$session.getUser()),
|
||||
countries: countries,
|
||||
|
@ -287,11 +328,6 @@ export default {
|
|||
},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if(this.isPublic && !this.isDemo) {
|
||||
this.$router.push({ name: "settings" });
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayName() {
|
||||
const user = this.$session.getUser();
|
||||
|
@ -302,6 +338,11 @@ export default {
|
|||
return this.$gettext("Unregistered");
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if(this.isPublic && !this.isDemo) {
|
||||
this.$router.push({ name: "settings" });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showDialog(name) {
|
||||
if (!name) {
|
||||
|
@ -312,6 +353,38 @@ export default {
|
|||
disabled() {
|
||||
return (this.isDemo || this.busy);
|
||||
},
|
||||
validEmail(v) {
|
||||
if (typeof v !== "string" || v === "") {
|
||||
return true;
|
||||
} else if (!this.validLength(v, 0, 250)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,32})+$/.test(v);
|
||||
},
|
||||
validLength(v, min, max) {
|
||||
if (typeof v !== "string" && min <= 0) {
|
||||
return true;
|
||||
} else if (max > 0 && v.length > max) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return v.length >= min;
|
||||
},
|
||||
validUrl(v) {
|
||||
if (typeof v !== "string" || v === "") {
|
||||
return true;
|
||||
} else if (!this.validLength(v, 0, 500)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(v);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onChangeAvatar() {
|
||||
if (this.busy) {
|
||||
return;
|
||||
|
@ -321,15 +394,19 @@ export default {
|
|||
onLogout() {
|
||||
this.$session.logout();
|
||||
},
|
||||
onChangeName() {
|
||||
this.user.Details.NameSrc = "manual";
|
||||
return this.onChange();
|
||||
},
|
||||
onChange() {
|
||||
if (this.busy) {
|
||||
if (this.busy || !this.valid) {
|
||||
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"));
|
||||
this.$notify.success(this.$gettext("Changes successfully saved"));
|
||||
}).finally(() => this.busy = false);
|
||||
},
|
||||
onUploadAvatar() {
|
||||
|
@ -339,12 +416,12 @@ export default {
|
|||
|
||||
this.busy = true;
|
||||
|
||||
Notify.info(this.$gettext("Uploading…"));
|
||||
Notify.info(this.$gettext("Updating picture…"));
|
||||
|
||||
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"));
|
||||
this.$notify.success(this.$gettext("Changes successfully saved"));
|
||||
}).finally(() => this.busy = false);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -369,7 +369,7 @@ export default {
|
|||
this.busy = true;
|
||||
|
||||
this.settings.save().then(() => {
|
||||
this.$notify.success(this.$gettext("Settings saved"));
|
||||
this.$notify.success(this.$gettext("Changes successfully saved"));
|
||||
}).finally(() => this.busy = false);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -457,7 +457,7 @@ export default {
|
|||
this.$notify.blockUI();
|
||||
setTimeout(() => window.location.reload(), 100);
|
||||
} else {
|
||||
this.$notify.success(this.$gettext("Settings saved"));
|
||||
this.$notify.success(this.$gettext("Changes successfully saved"));
|
||||
}
|
||||
}).finally(() => this.busy = false);
|
||||
},
|
||||
|
|
|
@ -175,7 +175,7 @@ export default {
|
|||
this.$notify.blockUI();
|
||||
setTimeout(() => window.location.reload(), 100);
|
||||
} else {
|
||||
this.$notify.success(this.$gettext("Settings saved"));
|
||||
this.$notify.success(this.$gettext("Changes successfully saved"));
|
||||
}
|
||||
}).finally(() => this.busy = false);
|
||||
},
|
||||
|
|
|
@ -21,11 +21,10 @@ describe("common/session", () => {
|
|||
const storage = new StorageShim();
|
||||
const session = new Session(storage, config);
|
||||
assert.equal(session.hasToken("2lbh9x09"), false);
|
||||
assert.equal(session.session_id, null);
|
||||
session.setId(123421);
|
||||
assert.equal(session.session_id, 123421);
|
||||
session.setId("999900000000000000000000000000000000000000000000");
|
||||
assert.equal(session.session_id, "999900000000000000000000000000000000000000000000");
|
||||
const result = session.getId();
|
||||
assert.equal(result, 123421);
|
||||
assert.equal(result, "999900000000000000000000000000000000000000000000");
|
||||
session.deleteId();
|
||||
assert.equal(session.session_id, null);
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ const clientConfig = {
|
|||
debug: false,
|
||||
readonly: false,
|
||||
uploadNSFW: false,
|
||||
public: true,
|
||||
public: false,
|
||||
experimental: true,
|
||||
disableSettings: false,
|
||||
test: true,
|
||||
|
|
|
@ -128,14 +128,40 @@ Mock.onDelete("api/v1/photos/pqbemz8276mhtobh/label/12345").reply(
|
|||
{ success: "ok" },
|
||||
mockHeaders
|
||||
);
|
||||
Mock.onPost("api/v1/session")
|
||||
.reply(200, { id: "8877", data: { user: { ID: 1, PrimaryEmail: "test@test.com" } } }, mockHeaders)
|
||||
.onDelete("api/v1/session/8877")
|
||||
.reply(200)
|
||||
.onDelete("api/v1/session/123")
|
||||
.reply(200);
|
||||
|
||||
Mock.onPost("api/v1/session").reply(200, { id: "123", data: { token: "123token" } }, mockHeaders);
|
||||
Mock.onPost("api/v1/session").reply(
|
||||
200,
|
||||
{
|
||||
id: "999900000000000000000000000000000000000000000000",
|
||||
data: { token: "123token" },
|
||||
user: { ID: 1, UID: "urjysof3b9v7lgex", Name: "test", Email: "test@test.com" },
|
||||
},
|
||||
mockHeaders
|
||||
);
|
||||
|
||||
Mock.onGet("api/v1/session/234200000000000000000000000000000000000000000000").reply(
|
||||
200,
|
||||
{
|
||||
id: "234200000000000000000000000000000000000000000000",
|
||||
data: { token: "123token" },
|
||||
user: { ID: 1, UID: "urjysof3b9v7lgex", Name: "test", Email: "test@test.com" },
|
||||
},
|
||||
mockHeaders
|
||||
);
|
||||
|
||||
Mock.onGet("api/v1/session/999900000000000000000000000000000000000000000000").reply(
|
||||
200,
|
||||
{
|
||||
id: "999900000000000000000000000000000000000000000000",
|
||||
data: { token: "123token" },
|
||||
user: { ID: 1, UID: "urjysof3b9v7lgex", Name: "test", Email: "test@test.com" },
|
||||
},
|
||||
mockHeaders
|
||||
);
|
||||
|
||||
Mock.onDelete("api/v1/session/999900000000000000000000000000000000000000000000").reply(200);
|
||||
|
||||
Mock.onDelete("api/v1/session/234200000000000000000000000000000000000000000000").reply(200);
|
||||
|
||||
Mock.onGet("api/v1/settings").reply(200, { download: true, language: "de" }, mockHeaders);
|
||||
Mock.onPost("api/v1/settings").reply(200, { download: true, language: "en" }, mockHeaders);
|
||||
|
|
|
@ -43,7 +43,7 @@ func CreateSession(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Check limit for failed auth requests (max. 10 per minute).
|
||||
if limiter.Auth.Reject(ClientIP(c)) {
|
||||
if limiter.Login.Reject(ClientIP(c)) {
|
||||
limiter.AbortJSON(c)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ func UploadUserAvatar(router *gin.RouterGroup) {
|
|||
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)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUnsupportedFormat)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -101,7 +101,7 @@ func UploadUserAvatar(router *gin.RouterGroup) {
|
|||
|
||||
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)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUnsupportedFormat)
|
||||
return
|
||||
} else if err = mediaFile.CreateThumbnails(conf.ThumbCachePath(), false); err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "%s"}, s.RefID, err)
|
||||
|
|
|
@ -29,7 +29,7 @@ func UpdateUserPassword(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Check limit for failed auth requests (max. 10 per minute).
|
||||
if limiter.Auth.Reject(ClientIP(c)) {
|
||||
if limiter.Login.Reject(ClientIP(c)) {
|
||||
limiter.AbortJSON(c)
|
||||
return
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ func UpdateUserPassword(router *gin.RouterGroup) {
|
|||
|
||||
// Verify that the old password is correct.
|
||||
if u.WrongPassword(f.OldPassword) {
|
||||
limiter.Auth.Reserve(ClientIP(c))
|
||||
limiter.Login.Reserve(ClientIP(c))
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrInvalidPassword)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -3,16 +3,14 @@ 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/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
||||
// UpdateUser updates the profile information of the currently authenticated user.
|
||||
|
@ -42,8 +40,8 @@ func UpdateUser(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
// 1) Init form with model values
|
||||
f, err := form.NewUser(m)
|
||||
// Init form with model values.
|
||||
f, err := m.Form()
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
@ -51,14 +49,14 @@ func UpdateUser(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
// 2) Update form with values from request
|
||||
// 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
|
||||
// Save model with values from form.
|
||||
if err = m.SaveForm(f); err != nil {
|
||||
log.Error(err)
|
||||
AbortSaveFailed(c)
|
||||
|
|
|
@ -2,6 +2,7 @@ package commands
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
|
@ -48,7 +49,7 @@ func showConfigAction(ctx *cli.Context) error {
|
|||
|
||||
// Show report.
|
||||
if opt.Format == report.Default {
|
||||
fmt.Printf("### %s ###\n\n", rep.Title)
|
||||
fmt.Printf("%s\n\n", strings.ToUpper(rep.Title))
|
||||
}
|
||||
|
||||
fmt.Println(result)
|
||||
|
|
|
@ -9,6 +9,8 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/report"
|
||||
|
||||
"github.com/sevlyar/go-daemon"
|
||||
"github.com/urfave/cli"
|
||||
|
||||
|
@ -26,7 +28,7 @@ import (
|
|||
var StartCommand = cli.Command{
|
||||
Name: "start",
|
||||
Aliases: []string{"up"},
|
||||
Usage: "Starts the web server",
|
||||
Usage: "Starts the Web server",
|
||||
Flags: startFlags,
|
||||
Action: startAction,
|
||||
}
|
||||
|
@ -53,13 +55,20 @@ func startAction(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
if ctx.IsSet("config") {
|
||||
fmt.Printf("Name Value\n")
|
||||
fmt.Printf("detach-server %t\n", conf.DetachServer())
|
||||
|
||||
fmt.Printf("http-host %s\n", conf.HttpHost())
|
||||
fmt.Printf("http-port %d\n", conf.HttpPort())
|
||||
fmt.Printf("http-mode %s\n", conf.HttpMode())
|
||||
// Create config report.
|
||||
cols := []string{"Name", "Value"}
|
||||
rows := [][]string{
|
||||
{"detach-server", fmt.Sprintf("%t", conf.DetachServer())},
|
||||
{"http-mode", conf.HttpMode()},
|
||||
{"http-compression", conf.HttpCompression()},
|
||||
{"http-host", conf.HttpHost()},
|
||||
{"http-port", fmt.Sprintf("%d", conf.HttpPort())},
|
||||
}
|
||||
|
||||
// Render config report.
|
||||
opt := report.Options{Format: report.CliFormat(ctx), NoWrap: true}
|
||||
result, _ := report.Render(rows, cols, opt)
|
||||
fmt.Printf("\n%s\n", result)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import (
|
|||
// StatusCommand registers the status command.
|
||||
var StatusCommand = cli.Command{
|
||||
Name: "status",
|
||||
Usage: "Checks if the web server is running",
|
||||
Usage: "Checks if the Web server is running",
|
||||
Action: statusAction,
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
var StopCommand = cli.Command{
|
||||
Name: "stop",
|
||||
Aliases: []string{"down"},
|
||||
Usage: "Stops the web server in daemon mode",
|
||||
Usage: "Stops the Web server in daemon mode",
|
||||
Action: stopAction,
|
||||
}
|
||||
|
||||
|
|
|
@ -89,12 +89,6 @@ 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 {
|
||||
|
@ -149,10 +143,10 @@ func (c *Config) CreateDirectories() error {
|
|||
return createError(c.ConfigPath(), err)
|
||||
}
|
||||
|
||||
if c.CertsPath() == "" {
|
||||
return notFoundError("certs")
|
||||
} else if err := os.MkdirAll(c.CertsPath(), os.ModePerm); err != nil {
|
||||
return createError(c.CertsPath(), err)
|
||||
if c.CertificatesPath() == "" {
|
||||
return notFoundError("certificates")
|
||||
} else if err := os.MkdirAll(c.CertificatesPath(), os.ModePerm); err != nil {
|
||||
return createError(c.CertificatesPath(), err)
|
||||
}
|
||||
|
||||
if c.TempPath() == "" {
|
||||
|
@ -310,31 +304,6 @@ 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 {
|
||||
|
|
|
@ -23,17 +23,6 @@ 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")
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
// CertsPath returns the path to the TLS certificates and keys.
|
||||
func (c *Config) CertsPath() string {
|
||||
return filepath.Join(c.ConfigPath(), "certs")
|
||||
}
|
||||
|
||||
// AutoTLS returns the email address for enabling automatic HTTPS via Let's Encrypt.
|
||||
func (c *Config) AutoTLS() string {
|
||||
return clean.Email(c.options.AutoTLS)
|
||||
}
|
||||
|
||||
// TLSKey returns the HTTPS private key filename.
|
||||
func (c *Config) TLSKey() string {
|
||||
if c.options.TLSKey == "" {
|
||||
return ""
|
||||
} else if fs.FileExistsNotEmpty(c.options.TLSKey) {
|
||||
return c.options.TLSKey
|
||||
} else if fileName := filepath.Join(c.CertsPath(), c.options.TLSKey); fs.FileExistsNotEmpty(fileName) {
|
||||
return fileName
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// TLSCert returns the HTTPS certificate filename.
|
||||
func (c *Config) TLSCert() string {
|
||||
if c.options.TLSCert == "" {
|
||||
return ""
|
||||
} else if fs.FileExistsNotEmpty(c.options.TLSCert) {
|
||||
return c.options.TLSCert
|
||||
} else if fileName := filepath.Join(c.CertsPath(), c.options.TLSCert); fs.FileExistsNotEmpty(fileName) {
|
||||
return fileName
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// TLS returns the HTTPS certificate and private key file name.
|
||||
func (c *Config) TLS() (certFile, privateKey string) {
|
||||
certFile = c.TLSCert()
|
||||
privateKey = c.TLSKey()
|
||||
|
||||
if c.options.TLSCert == "" || privateKey == "" {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return certFile, privateKey
|
||||
}
|
||||
|
||||
// HttpsPort returns the HTTPS server port number.
|
||||
func (c *Config) HttpsPort() int {
|
||||
if !c.SiteHttps() {
|
||||
return -1
|
||||
}
|
||||
|
||||
if c.options.HttpsPort == 0 {
|
||||
return 2443
|
||||
}
|
||||
|
||||
return c.options.HttpsPort
|
||||
}
|
||||
|
||||
// HttpsRedirect returns the HTTPS redirect status code.
|
||||
func (c *Config) HttpsRedirect() int {
|
||||
if !c.SiteHttps() {
|
||||
return -1
|
||||
}
|
||||
|
||||
if c.options.HttpsRedirect > 0 && c.options.HttpsRedirect < 300 && c.options.HttpsRedirect >= 400 {
|
||||
return 301
|
||||
}
|
||||
|
||||
return c.options.HttpsRedirect
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConfig_CertsPath(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
if dir := c.CertsPath(); dir == "" {
|
||||
t.Fatal("certs path is empty")
|
||||
} else if !strings.HasPrefix(dir, c.ConfigPath()) {
|
||||
t.Fatalf("unexpected certs path: %s", dir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_AutoTLS(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "", c.AutoTLS())
|
||||
c.options.AutoTLS = "hello@example.com"
|
||||
assert.Equal(t, "hello@example.com", c.AutoTLS())
|
||||
c.options.AutoTLS = "hello"
|
||||
assert.Equal(t, "", c.AutoTLS())
|
||||
c.options.AutoTLS = ""
|
||||
assert.Equal(t, "", c.AutoTLS())
|
||||
}
|
||||
|
||||
func TestConfig_TLS(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
cert, key := c.TLS()
|
||||
|
||||
assert.Equal(t, "", cert)
|
||||
assert.Equal(t, "", key)
|
||||
}
|
||||
|
||||
func TestConfig_TLSKey(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "", c.TLSKey())
|
||||
}
|
||||
|
||||
func TestConfig_TLSCert(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "", c.TLSCert())
|
||||
}
|
||||
|
||||
func TestConfig_HttpsPort(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, -1, c.HttpsPort())
|
||||
}
|
||||
|
||||
func TestConfig_HttpsRedirect(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, -1, c.HttpsRedirect())
|
||||
}
|
86
internal/config/config_tls.go
Normal file
86
internal/config/config_tls.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
const (
|
||||
PrivateKeyExt = ".key"
|
||||
PublicCertExt = ".crt"
|
||||
)
|
||||
|
||||
// CertificatesPath returns the path to the TLS certificates and keys.
|
||||
func (c *Config) CertificatesPath() string {
|
||||
return filepath.Join(c.ConfigPath(), "certificates")
|
||||
}
|
||||
|
||||
// TLSEmail returns the email address to enable automatic HTTPS via Let's Encrypt
|
||||
func (c *Config) TLSEmail() string {
|
||||
return clean.Email(c.options.TLSEmail)
|
||||
}
|
||||
|
||||
// TLSCert returns the public certificate required to enable TLS.
|
||||
func (c *Config) TLSCert() string {
|
||||
certName := c.options.TLSCert
|
||||
if certName == "" {
|
||||
certName = c.SiteDomain() + PublicCertExt
|
||||
} else if fs.FileExistsNotEmpty(certName) {
|
||||
return certName
|
||||
}
|
||||
|
||||
// Find and return public certificate.
|
||||
if fileName := filepath.Join(c.CertificatesPath(), certName); fs.FileExistsNotEmpty(fileName) {
|
||||
return fileName
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// TLSKey returns the private key required to enable TLS.
|
||||
func (c *Config) TLSKey() string {
|
||||
keyName := c.options.TLSKey
|
||||
|
||||
if keyName == "" {
|
||||
keyName = c.SiteDomain() + PrivateKeyExt
|
||||
} else if fs.FileExistsNotEmpty(keyName) {
|
||||
return keyName
|
||||
}
|
||||
|
||||
// Find and return private key.
|
||||
if fileName := filepath.Join(c.CertificatesPath(), keyName); fs.FileExistsNotEmpty(fileName) {
|
||||
return fileName
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// TLS returns the HTTPS certificate and private key file name.
|
||||
func (c *Config) TLS() (publicCert, privateKey string) {
|
||||
privateKey = c.TLSKey()
|
||||
|
||||
if privateKey == "" {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
publicCert = c.TLSCert()
|
||||
|
||||
if publicCert == "" {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return publicCert, privateKey
|
||||
}
|
||||
|
||||
// DisableTLS checks if HTTPS should be disabled.
|
||||
func (c *Config) DisableTLS() bool {
|
||||
if c.options.DisableTLS {
|
||||
return true
|
||||
} else if !c.SiteHttps() {
|
||||
return true
|
||||
}
|
||||
|
||||
return c.TLSCert() == "" || c.TLSKey() == ""
|
||||
}
|
56
internal/config/config_tls_test.go
Normal file
56
internal/config/config_tls_test.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConfig_CertificatesPath(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
if dir := c.CertificatesPath(); dir == "" {
|
||||
t.Fatal("certificates path is empty")
|
||||
} else if !strings.HasPrefix(dir, c.ConfigPath()) {
|
||||
t.Fatalf("unexpected certificates path: %s", dir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_TLSEmail(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "", c.TLSEmail())
|
||||
c.options.TLSEmail = "hello@example.com"
|
||||
assert.Equal(t, "hello@example.com", c.TLSEmail())
|
||||
c.options.TLSEmail = "hello"
|
||||
assert.Equal(t, "", c.TLSEmail())
|
||||
c.options.TLSEmail = ""
|
||||
assert.Equal(t, "", c.TLSEmail())
|
||||
}
|
||||
|
||||
func TestConfig_TLSCert(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "", c.TLSCert())
|
||||
}
|
||||
|
||||
func TestConfig_TLSKey(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "", c.TLSKey())
|
||||
}
|
||||
|
||||
func TestConfig_TLS(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
cert, key := c.TLS()
|
||||
|
||||
assert.Equal(t, "", cert)
|
||||
assert.Equal(t, "", key)
|
||||
}
|
||||
|
||||
func TestConfig_DisableTLS(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.True(t, c.DisableTLS())
|
||||
}
|
|
@ -397,39 +397,6 @@ var Flags = CliFlags{
|
|||
Value: &cli.StringSlice{header.CidrDockerInternal},
|
||||
EnvVar: "PHOTOPRISM_TRUSTED_PROXY",
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "http-mode, mode",
|
||||
Usage: "HTTP server `MODE` (debug, release, or test)",
|
||||
EnvVar: "PHOTOPRISM_HTTP_MODE",
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "http-compression, z",
|
||||
Usage: "HTTP server compression `METHOD` (none or gzip)",
|
||||
EnvVar: "PHOTOPRISM_HTTP_COMPRESSION",
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "http-host, ip",
|
||||
Usage: "HTTP server `IP` address",
|
||||
EnvVar: "PHOTOPRISM_HTTP_HOST",
|
||||
}}, {
|
||||
Flag: cli.IntFlag{
|
||||
Name: "http-port, port",
|
||||
Value: 2342,
|
||||
Usage: "HTTP server port `NUMBER`",
|
||||
EnvVar: "PHOTOPRISM_HTTP_PORT",
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "auto-tls",
|
||||
Usage: "`EMAIL` address to enable automatic HTTPS via Let's Encrypt",
|
||||
EnvVar: "PHOTOPRISM_AUTO_TLS",
|
||||
Hidden: true,
|
||||
}}, {
|
||||
Flag: cli.IntFlag{
|
||||
Name: "https-port",
|
||||
Value: 2443,
|
||||
Usage: "HTTPS server port `NUMBER` if TLS is configured",
|
||||
EnvVar: "PHOTOPRISM_HTTPS_PORT",
|
||||
}}, {
|
||||
Flag: cli.StringSliceFlag{
|
||||
Name: "https-proxy-header",
|
||||
Usage: "proxy protocol header `NAME`",
|
||||
|
@ -442,11 +409,26 @@ var Flags = CliFlags{
|
|||
Value: &cli.StringSlice{header.ProtoHttps},
|
||||
EnvVar: "PHOTOPRISM_HTTPS_PROXY_PROTO",
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "http-mode, mode",
|
||||
Usage: "Web server `MODE` (debug, release, or test)",
|
||||
EnvVar: "PHOTOPRISM_HTTP_MODE",
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "http-compression, z",
|
||||
Usage: "Web server compression `METHOD` (none or gzip)",
|
||||
EnvVar: "PHOTOPRISM_HTTP_COMPRESSION",
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "http-host, ip",
|
||||
Usage: "Web server `IP` address",
|
||||
EnvVar: "PHOTOPRISM_HTTP_HOST",
|
||||
}}, {
|
||||
Flag: cli.IntFlag{
|
||||
Name: "https-redirect",
|
||||
Value: 0,
|
||||
Usage: "status `CODE` when redirecting from HTTP to HTTPS (300-399)",
|
||||
EnvVar: "PHOTOPRISM_HTTPS_REDIRECT",
|
||||
Name: "http-port, port",
|
||||
Value: 2342,
|
||||
Usage: "Web server port `NUMBER`",
|
||||
EnvVar: "PHOTOPRISM_HTTP_PORT",
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "database-driver, db",
|
||||
|
|
|
@ -45,7 +45,6 @@ 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"`
|
||||
|
@ -92,17 +91,16 @@ type Options struct {
|
|||
SiteDescription string `yaml:"SiteDescription" json:"SiteDescription" flag:"site-description"`
|
||||
SitePreview string `yaml:"SitePreview" json:"SitePreview" flag:"site-preview"`
|
||||
TrustedProxies []string `yaml:"TrustedProxies" json:"-" flag:"trusted-proxy"`
|
||||
HttpsProxyHeaders []string `yaml:"HttpsProxyHeaders" json:"-" flag:"https-proxy-header"`
|
||||
HttpsProxyProto []string `yaml:"HttpsProxyProto" json:"-" flag:"https-proxy-proto"`
|
||||
HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"`
|
||||
HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"`
|
||||
HttpHost string `yaml:"HttpHost" json:"-" flag:"http-host"`
|
||||
HttpPort int `yaml:"HttpPort" json:"-" flag:"http-port"`
|
||||
AutoTLS string `yaml:"AutoTLS" json:"AutoTLS" flag:"auto-tls"` // AutoTLS enabled automatic HTTPS via Let's Encrypt if set a valid email address.
|
||||
TLSKey string `yaml:"TLSKey" json:"TLSKey" flag:"tls-key"`
|
||||
TLSEmail string `yaml:"TLSEmail" json:"TLSEmail" flag:"tls-email"` // TLSEmail enabled automatic HTTPS via Let's Encrypt if set a valid email address.
|
||||
TLSCert string `yaml:"TLSCert" json:"TLSCert" flag:"tls-cert"`
|
||||
HttpsPort int `yaml:"HttpsPort" json:"HttpsPort" flag:"https-port"` // HttpsPort is the port number to be used for HTTPS connections.
|
||||
HttpsProxyHeaders []string `yaml:"HttpsProxyHeaders" json:"-" flag:"https-proxy-header"`
|
||||
HttpsProxyProto []string `yaml:"HttpsProxyProto" json:"-" flag:"https-proxy-proto"`
|
||||
HttpsRedirect int `yaml:"HttpsRedirect" json:"HttpsRedirect" flag:"https-redirect"`
|
||||
TLSKey string `yaml:"TLSKey" json:"TLSKey" flag:"tls-key"`
|
||||
DisableTLS bool `yaml:"DisableTLS" json:"DisableTLS" flag:"disable-tls"`
|
||||
DatabaseDriver string `yaml:"DatabaseDriver" json:"-" flag:"database-driver"`
|
||||
DatabaseDsn string `yaml:"DatabaseDsn" json:"-" flag:"database-dsn"`
|
||||
DatabaseName string `yaml:"DatabaseName" json:"-" flag:"database-name"`
|
||||
|
|
|
@ -35,7 +35,7 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||
|
||||
// Config.
|
||||
{"config-path", c.ConfigPath()},
|
||||
{"certs-path", c.CertsPath()},
|
||||
{"certificates-path", c.CertificatesPath()},
|
||||
{"options-yaml", c.OptionsYaml()},
|
||||
{"defaults-yaml", c.DefaultsYaml()},
|
||||
{"settings-yaml", c.SettingsYaml()},
|
||||
|
@ -48,7 +48,6 @@ 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()},
|
||||
|
@ -109,6 +108,8 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||
// Site Infos.
|
||||
{"cdn-url", c.CdnUrl("/")},
|
||||
{"site-url", c.SiteUrl()},
|
||||
{"site-https", fmt.Sprintf("%t", c.SiteHttps())},
|
||||
{"site-domain", c.SiteDomain()},
|
||||
{"site-author", c.SiteAuthor()},
|
||||
{"site-title", c.SiteTitle()},
|
||||
{"site-caption", c.SiteCaption()},
|
||||
|
@ -127,17 +128,18 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||
|
||||
// HTTP(S) Proxy.
|
||||
{"trusted-proxy", c.TrustedProxy()},
|
||||
{"https-proxy-header", strings.Join(c.HttpsProxyHeader(), ", ")},
|
||||
{"https-proxy-proto", strings.Join(c.HttpsProxyProto(), ", ")},
|
||||
|
||||
// Web Server.
|
||||
{"http-mode", c.HttpMode()},
|
||||
{"http-compression", c.HttpCompression()},
|
||||
{"http-host", c.HttpHost()},
|
||||
{"http-port", fmt.Sprintf("%d", c.HttpPort())},
|
||||
{"auto-tls", c.AutoTLS()},
|
||||
{"https-port", fmt.Sprintf("%d", c.HttpsPort())},
|
||||
{"https-proxy-header", strings.Join(c.HttpsProxyHeader(), ", ")},
|
||||
{"https-proxy-proto", strings.Join(c.HttpsProxyProto(), ", ")},
|
||||
{"https-redirect", fmt.Sprintf("%d", c.HttpsRedirect())},
|
||||
{"tls-email", c.TLSEmail()},
|
||||
{"tls-cert", c.TLSCert()},
|
||||
{"tls-key", c.TLSKey()},
|
||||
{"disable-tls", fmt.Sprintf("%t", c.DisableTLS())},
|
||||
|
||||
// Database.
|
||||
{"database-driver", c.DatabaseDriver()},
|
||||
|
|
|
@ -33,7 +33,7 @@ func (m *Session) LogIn(f form.Login, c *gin.Context) (err error) {
|
|||
// User found?
|
||||
if user == nil {
|
||||
message := "account not found"
|
||||
limiter.Auth.Reserve(m.IP())
|
||||
limiter.Login.Reserve(m.IP())
|
||||
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
|
@ -52,7 +52,7 @@ func (m *Session) LogIn(f form.Login, c *gin.Context) (err error) {
|
|||
// Password valid?
|
||||
if user.WrongPassword(f.Password) {
|
||||
message := "incorrect password"
|
||||
limiter.Auth.Reserve(m.IP())
|
||||
limiter.Login.Reserve(m.IP())
|
||||
event.AuditErr([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
|
@ -72,7 +72,7 @@ func (m *Session) LogIn(f form.Login, c *gin.Context) (err error) {
|
|||
// Redeem token.
|
||||
if user.IsRegistered() {
|
||||
if shares := user.RedeemToken(f.AuthToken); shares == 0 {
|
||||
limiter.Auth.Reserve(m.IP())
|
||||
limiter.Login.Reserve(m.IP())
|
||||
event.AuditWarn([]string{m.IP(), "session %s", "share token %s is invalid"}, m.RefID, clean.LogQuote(f.AuthToken))
|
||||
m.Status = http.StatusNotFound
|
||||
return i18n.Error(i18n.ErrInvalidLink)
|
||||
|
@ -83,7 +83,7 @@ func (m *Session) LogIn(f form.Login, c *gin.Context) (err error) {
|
|||
m.Status = http.StatusInternalServerError
|
||||
return i18n.Error(i18n.ErrUnexpected)
|
||||
} else if shares := data.RedeemToken(f.AuthToken); shares == 0 {
|
||||
limiter.Auth.Reserve(m.IP())
|
||||
limiter.Login.Reserve(m.IP())
|
||||
event.AuditWarn([]string{m.IP(), "session %s", "share token %s is invalid"}, m.RefID, clean.LogQuote(f.AuthToken))
|
||||
event.LoginError(m.IP(), "api", "", m.UserAgent, "invalid share token")
|
||||
m.Status = http.StatusNotFound
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
||||
"github.com/ulule/deepcopier"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
@ -115,6 +117,7 @@ func FindUser(find User) *User {
|
|||
|
||||
// Find matching record.
|
||||
if err := stmt.First(m).Error; err != nil {
|
||||
event.AuditErr([]string{"user", "not found", "%s"}, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -385,18 +388,26 @@ func (m *User) Name() string {
|
|||
|
||||
// SetName sets the login username to the specified string.
|
||||
func (m *User) SetName(login string) (err error) {
|
||||
if m.ID < 0 {
|
||||
return fmt.Errorf("system users cannot be modified")
|
||||
}
|
||||
|
||||
login = clean.Username(login)
|
||||
|
||||
// Empty?
|
||||
if login == "" {
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
return fmt.Errorf("username is empty")
|
||||
} else if m.UserName == login {
|
||||
return nil
|
||||
} else if m.UserName != "" && m.ID != 1 {
|
||||
return fmt.Errorf("username cannot be changed")
|
||||
}
|
||||
|
||||
// Update username and slug.
|
||||
m.UserName = login
|
||||
|
||||
// Update display name.
|
||||
if m.DisplayName == "" || m.DisplayName == AdminDisplayName {
|
||||
if m.DisplayName == "" || m.DisplayName == AdminDisplayName && m.ID == 1 {
|
||||
m.DisplayName = clean.NameCapitalized(login)
|
||||
}
|
||||
|
||||
|
@ -767,20 +778,43 @@ func (m *User) RedeemToken(token string) (n int) {
|
|||
return n
|
||||
}
|
||||
|
||||
// Form returns a populated user form to perform changes.
|
||||
func (m *User) Form() (form.User, error) {
|
||||
frm := form.User{UserDetails: &form.UserDetails{}}
|
||||
|
||||
if err := deepcopier.Copy(m).To(&frm); err != nil {
|
||||
return frm, err
|
||||
}
|
||||
|
||||
if err := deepcopier.Copy(m.UserDetails).To(frm.UserDetails); err != nil {
|
||||
return frm, err
|
||||
}
|
||||
|
||||
return frm, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Ignore details if not set.
|
||||
if f.UserDetails == nil {
|
||||
// Ignore.
|
||||
} else if err := deepcopier.Copy(f.UserDetails).To(m.UserDetails); err != nil {
|
||||
return err
|
||||
} else {
|
||||
m.UserDetails.UserAbout = strings.TrimSpace(m.UserDetails.UserAbout)
|
||||
m.UserDetails.UserBio = strings.TrimSpace(m.UserDetails.UserBio)
|
||||
}
|
||||
|
||||
// Sanitize display name.
|
||||
if n := clean.Name(f.DisplayName); n != "" && n != m.DisplayName {
|
||||
m.DisplayName = n
|
||||
m.SetDisplayName(n)
|
||||
}
|
||||
|
||||
// Sanitize email address.
|
||||
if email := clean.Email(f.UserEmail); email != "" && email != m.UserEmail {
|
||||
m.UserEmail = email
|
||||
m.VerifiedAt = nil
|
||||
|
@ -790,6 +824,36 @@ func (m *User) SaveForm(f form.User) error {
|
|||
return m.Save()
|
||||
}
|
||||
|
||||
// SetDisplayName sets a new display name and, if possible, splits it into its components.
|
||||
func (m *User) SetDisplayName(name string) *User {
|
||||
name = clean.Name(name)
|
||||
|
||||
// Empty?
|
||||
if name == "" {
|
||||
return m
|
||||
}
|
||||
|
||||
m.DisplayName = name
|
||||
|
||||
d := m.Details()
|
||||
|
||||
if SrcPriority[SrcAuto] < SrcPriority[d.NameSrc] {
|
||||
return m
|
||||
}
|
||||
|
||||
// Try to parse name into components.
|
||||
n := txt.ParseName(name)
|
||||
|
||||
d.NameTitle = n.Title
|
||||
d.GivenName = n.Given
|
||||
d.MiddleName = n.Middle
|
||||
d.FamilyName = n.Family
|
||||
d.NameSuffix = n.Suffix
|
||||
d.NickName = n.Nick
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// SetAvatar updates the user avatar image.
|
||||
func (m *User) SetAvatar(thumb, thumbSrc string) error {
|
||||
if m.UserName == "" || m.ID <= 0 {
|
||||
|
|
|
@ -17,22 +17,23 @@ const (
|
|||
// UserDetails represents user profile information.
|
||||
type UserDetails struct {
|
||||
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"`
|
||||
BirthYear int `json:"BirthYear" yaml:"BirthYear,omitempty"`
|
||||
BirthMonth int `json:"BirthMonth" yaml:"BirthMonth,omitempty"`
|
||||
BirthDay int `json:"BirthDay" yaml:"BirthDay,omitempty"`
|
||||
NameTitle string `gorm:"size:32;" json:"NameTitle" yaml:"NameTitle,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"`
|
||||
NameSrc string `gorm:"type:VARBINARY(8);" json:"NameSrc" yaml:"NameSrc,omitempty"`
|
||||
UserGender string `gorm:"size:16;" json:"Gender" yaml:"Gender,omitempty"`
|
||||
UserAbout string `gorm:"size:512;" json:"About" yaml:"About,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"`
|
||||
|
@ -41,11 +42,12 @@ type UserDetails struct {
|
|||
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"`
|
||||
OrgName string `gorm:"size:128;" json:"OrgName" yaml:"OrgName,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"`
|
||||
IdURL string `gorm:"type:VARBINARY(512);column:id_url;" json:"IdURL,omitempty" yaml:"IdURL,omitempty"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
||||
}
|
||||
|
|
|
@ -117,12 +117,9 @@ func TestUser_Create(t *testing.T) {
|
|||
assert.Equal(t, "example", m.Name())
|
||||
assert.Equal(t, "example", m.UserName)
|
||||
|
||||
if err := m.UpdateName("example-editor"); err != nil {
|
||||
t.Fatal(err)
|
||||
if err := m.UpdateName("example-editor"); err == nil {
|
||||
t.Fatal("error expected")
|
||||
}
|
||||
|
||||
assert.Equal(t, "example-editor", m.Name())
|
||||
assert.Equal(t, "example-editor", m.UserName)
|
||||
})
|
||||
t.Run("NewUser", func(t *testing.T) {
|
||||
if err := NewUser().Create(); err != nil {
|
||||
|
@ -612,9 +609,9 @@ func TestUser_Validate(t *testing.T) {
|
|||
})
|
||||
t.Run("RoleEmpty", func(t *testing.T) {
|
||||
u := &User{
|
||||
UserName: "jens.mander",
|
||||
UserEmail: "jens@mander.de",
|
||||
DisplayName: "Jens Mander",
|
||||
UserName: "test.example",
|
||||
UserEmail: "test@example.com",
|
||||
DisplayName: "Test Example",
|
||||
UserRole: "",
|
||||
}
|
||||
|
||||
|
@ -622,9 +619,9 @@ func TestUser_Validate(t *testing.T) {
|
|||
})
|
||||
t.Run("RoleAdmin", func(t *testing.T) {
|
||||
u := &User{
|
||||
UserName: "jens.mander",
|
||||
UserEmail: "jens@mander.de",
|
||||
DisplayName: "Jens Mander",
|
||||
UserName: "test.example",
|
||||
UserEmail: "test@example.com",
|
||||
DisplayName: "Test Example",
|
||||
UserRole: acl.RoleAdmin.String(),
|
||||
}
|
||||
|
||||
|
@ -632,9 +629,9 @@ func TestUser_Validate(t *testing.T) {
|
|||
})
|
||||
t.Run("RoleInvalid", func(t *testing.T) {
|
||||
u := &User{
|
||||
UserName: "jens.mander",
|
||||
UserEmail: "jens@mander.de",
|
||||
DisplayName: "Jens Mander",
|
||||
UserName: "test.example",
|
||||
UserEmail: "test@example.com",
|
||||
DisplayName: "Test Example",
|
||||
UserRole: "foobar",
|
||||
}
|
||||
|
||||
|
@ -736,31 +733,74 @@ func TestUser_SharedUIDs(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestUser_Form(t *testing.T) {
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
m := FindUserByName("alice")
|
||||
|
||||
if m == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
|
||||
frm, err := m.Form()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("User: %#v", m)
|
||||
t.Logf("User.UserDetails: %#v", m.UserDetails)
|
||||
t.Logf("Form: %#v", frm)
|
||||
t.Logf("Form.UserDetails: %#v", frm.UserDetails)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_SaveForm(t *testing.T) {
|
||||
t.Run("UnknownUser", func(t *testing.T) {
|
||||
frm, err := form.NewUser(UnknownUser)
|
||||
frm, err := UnknownUser.Form()
|
||||
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)
|
||||
m := FindUser(Admin)
|
||||
|
||||
if m == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
|
||||
frm, err := m.Form()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(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)
|
||||
m = FindUserByUID(Admin.UserUID)
|
||||
assert.Equal(t, "admin@example.com", m.UserEmail)
|
||||
assert.Equal(t, "GoLand", m.Details().UserLocation)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_SetDisplayName(t *testing.T) {
|
||||
t.Run("BillGates", func(t *testing.T) {
|
||||
user := NewUser()
|
||||
user.SetDisplayName("Sir William Henry Gates III")
|
||||
d := user.Details()
|
||||
assert.Equal(t, "Sir", d.NameTitle)
|
||||
assert.Equal(t, "William", d.GivenName)
|
||||
assert.Equal(t, "Henry Gates", d.FamilyName)
|
||||
assert.Equal(t, "III", d.NameSuffix)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_SetAvatar(t *testing.T) {
|
||||
t.Run("Visitor", func(t *testing.T) {
|
||||
err := Visitor.SetAvatar("ebfc0aea7d3fd018b5fff57c76806b35181855ed", SrcManual)
|
||||
|
|
|
@ -104,12 +104,19 @@ class auth_users_details {
|
|||
varbinary(42) place_id
|
||||
varbinary(8) place_src
|
||||
varbinary(42) cell_id
|
||||
int(11) birth_year
|
||||
int(11) birth_month
|
||||
int(11) birth_day
|
||||
varchar(512) user_bio
|
||||
varchar(32) user_phone
|
||||
varchar(512) user_location
|
||||
varbinary(2) user_country
|
||||
varchar(32) user_phone
|
||||
varbinary(512) site_url
|
||||
varbinary(512) profile_url
|
||||
varbinary(512) feed_url
|
||||
varbinary(512) avatar_url
|
||||
varchar(16) user_gender
|
||||
varchar(32) name_prefix
|
||||
varchar(32) name_title
|
||||
varchar(64) given_name
|
||||
varchar(64) middle_name
|
||||
varchar(64) family_name
|
||||
|
@ -119,12 +126,6 @@ class auth_users_details {
|
|||
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
|
||||
datetime created_at
|
||||
datetime updated_at
|
||||
varbinary(42) user_uid
|
||||
|
|
|
@ -158,12 +158,19 @@ CREATE TABLE `auth_users_details` (
|
|||
`place_id` varbinary(42) DEFAULT 'zz',
|
||||
`place_src` varbinary(8) DEFAULT NULL,
|
||||
`cell_id` varbinary(42) DEFAULT 'zz',
|
||||
`birth_year` int(11) DEFAULT -1,
|
||||
`birth_month` int(11) DEFAULT -1,
|
||||
`birth_day` int(11) DEFAULT -1,
|
||||
`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_phone` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`site_url` varbinary(512) DEFAULT NULL,
|
||||
`profile_url` varbinary(512) DEFAULT NULL,
|
||||
`feed_url` varbinary(512) DEFAULT NULL,
|
||||
`avatar_url` varbinary(512) DEFAULT NULL,
|
||||
`user_gender` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`name_prefix` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`name_title` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`given_name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`middle_name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`family_name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
|
@ -171,14 +178,10 @@ CREATE TABLE `auth_users_details` (
|
|||
`nick_name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`org_title` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`org_name` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`org_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`org_phone` varchar(32) 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,
|
||||
`created_at` datetime DEFAULT NULL,
|
||||
`updated_at` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`user_uid`),
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package form
|
||||
|
||||
import (
|
||||
"github.com/ulule/deepcopier"
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
|
@ -9,24 +8,18 @@ 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"`
|
||||
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
|
||||
UserName string `json:"Name,omitempty" 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,omitempty"`
|
||||
}
|
||||
|
||||
// NewUserFromCli creates a new form with values from a CLI context.
|
||||
|
|
|
@ -5,13 +5,15 @@ type UserDetails struct {
|
|||
BirthYear int `json:"BirthYear"`
|
||||
BirthMonth int `json:"BirthMonth"`
|
||||
BirthDay int `json:"BirthDay"`
|
||||
NamePrefix string `json:"NamePrefix"`
|
||||
NameTitle string `json:"NameTitle"`
|
||||
GivenName string `json:"GivenName"`
|
||||
MiddleName string `json:"MiddleName"`
|
||||
FamilyName string `json:"FamilyName"`
|
||||
NameSuffix string `json:"NameSuffix"`
|
||||
NickName string `json:"NickName"`
|
||||
NameSrc string `json:"NameSrc"`
|
||||
UserGender string `json:"Gender"`
|
||||
UserAbout string `json:"About"`
|
||||
UserBio string `json:"Bio"`
|
||||
UserLocation string `json:"Location"`
|
||||
UserCountry string `json:"Country"`
|
||||
|
@ -19,8 +21,8 @@ type UserDetails struct {
|
|||
SiteURL string `json:"SiteURL"`
|
||||
ProfileURL string `json:"ProfileURL"`
|
||||
FeedURL string `json:"FeedURL"`
|
||||
OrgName string `json:"OrgName"`
|
||||
OrgTitle string `json:"OrgTitle"`
|
||||
OrgName string `json:"OrgName"`
|
||||
OrgEmail string `json:"OrgEmail"`
|
||||
OrgPhone string `json:"OrgPhone"`
|
||||
OrgURL string `json:"OrgURL"`
|
||||
|
|
|
@ -14,19 +14,6 @@ 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"}
|
||||
|
|
|
@ -9,7 +9,7 @@ const (
|
|||
ErrNotFound
|
||||
ErrFileNotFound
|
||||
ErrFileTooLarge
|
||||
ErrWrongFileType
|
||||
ErrUnsupportedFormat
|
||||
ErrOriginalsEmpty
|
||||
ErrSelectionNotFound
|
||||
ErrEntityNotFound
|
||||
|
@ -99,7 +99,7 @@ var Messages = MessageMap{
|
|||
ErrNotFound: gettext("Not found"),
|
||||
ErrFileNotFound: gettext("File not found"),
|
||||
ErrFileTooLarge: gettext("File too large"),
|
||||
ErrWrongFileType: gettext("Wrong file type"),
|
||||
ErrUnsupportedFormat: gettext("Unsupported format"),
|
||||
ErrOriginalsEmpty: gettext("Originals folder is empty"),
|
||||
ErrSelectionNotFound: gettext("Selection not found"),
|
||||
ErrEntityNotFound: gettext("Entity not found"),
|
||||
|
|
|
@ -139,6 +139,6 @@ var DialectMySQL = Migrations{
|
|||
ID: "20221015-100100",
|
||||
Dialect: "mysql",
|
||||
Stage: "pre",
|
||||
Statements: []string{"ALTER TABLE files_sync CHANGE account_id service_id INT UNSIGNED NOT NULL;", "ALTER TABLE files_share CHANGE account_id service_id INT UNSIGNED NOT NULL;"},
|
||||
Statements: []string{"ALTER IGNORE TABLE files_sync CHANGE account_id service_id INT UNSIGNED NOT NULL;", "ALTER IGNORE TABLE files_share CHANGE account_id service_id INT UNSIGNED NOT NULL;"},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
ALTER TABLE files_sync CHANGE account_id service_id INT UNSIGNED NOT NULL;
|
||||
ALTER TABLE files_share CHANGE account_id service_id INT UNSIGNED NOT NULL;
|
||||
ALTER IGNORE TABLE files_sync CHANGE account_id service_id INT UNSIGNED NOT NULL;
|
||||
ALTER IGNORE TABLE files_share CHANGE account_id service_id INT UNSIGNED NOT NULL;
|
|
@ -18,10 +18,10 @@ func AutoTLS(conf *config.Config) (*autocert.Manager, error) {
|
|||
return nil, fmt.Errorf("default site url does not use https")
|
||||
} else if siteDomain = conf.SiteDomain(); !strings.Contains(siteDomain, ".") {
|
||||
return nil, fmt.Errorf("no fully qualified site domain")
|
||||
} else if tlsEmail = conf.AutoTLS(); tlsEmail == "" {
|
||||
} else if tlsEmail = conf.TLSEmail(); tlsEmail == "" {
|
||||
return nil, fmt.Errorf("automatic tls disabled")
|
||||
} else if certDir = conf.CertsPath(); certDir == "" {
|
||||
return nil, fmt.Errorf("certs path not found")
|
||||
} else if certDir = conf.CertificatesPath(); certDir == "" {
|
||||
return nil, fmt.Errorf("certificates path not found")
|
||||
}
|
||||
|
||||
// Create Let's Encrypt cert manager.
|
||||
|
|
|
@ -76,7 +76,7 @@ func BasicAuth() gin.HandlerFunc {
|
|||
clientIp := api.ClientIP(c)
|
||||
|
||||
// Check limit for failed auth requests (max. 10 per minute).
|
||||
if limiter.Auth.Reject(clientIp) {
|
||||
if limiter.Login.Reject(clientIp) {
|
||||
limiter.Abort(c)
|
||||
return
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ func BasicAuth() gin.HandlerFunc {
|
|||
// Username not found.
|
||||
message := "account not found"
|
||||
|
||||
limiter.Auth.Reserve(clientIp)
|
||||
limiter.Login.Reserve(clientIp)
|
||||
event.AuditWarn([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(name))
|
||||
event.LoginError(clientIp, "webdav", name, api.UserAgent(c), message)
|
||||
} else if !user.CanUseWebDAV() {
|
||||
|
@ -102,7 +102,7 @@ func BasicAuth() gin.HandlerFunc {
|
|||
// Wrong password.
|
||||
message := "incorrect password"
|
||||
|
||||
limiter.Auth.Reserve(clientIp)
|
||||
limiter.Login.Reserve(clientIp)
|
||||
event.AuditErr([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(name))
|
||||
event.LoginError(clientIp, "webdav", name, api.UserAgent(c), message)
|
||||
} else {
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
package limiter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
const DefaultAuthLimit = 10
|
||||
const DefaultAuthInterval = time.Minute
|
||||
|
||||
// Auth limits failed authentication requests (one per minute).
|
||||
var Auth = NewLimit(rate.Every(DefaultAuthInterval), DefaultAuthLimit)
|
|
@ -1,60 +0,0 @@
|
|||
package limiter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAuth(t *testing.T) {
|
||||
clientIp := "192.0.2.42"
|
||||
|
||||
for i := 0; i < 9; i++ {
|
||||
t.Logf("tokens now: %f", Auth.IP(clientIp).TokensAt(time.Now()))
|
||||
assert.True(t, Auth.IP(clientIp).Allow())
|
||||
}
|
||||
|
||||
assert.True(t, Auth.IP(clientIp).Allow())
|
||||
assert.False(t, Auth.IP(clientIp).Allow())
|
||||
assert.False(t, Auth.IP(clientIp).Allow())
|
||||
assert.False(t, Auth.IP(clientIp).Allow())
|
||||
|
||||
t.Logf("tokens now: %f", Auth.IP(clientIp).TokensAt(time.Now()))
|
||||
t.Logf("tokens +1min: %f", Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute)))
|
||||
t.Logf("tokens +2min: %f", Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*2)))
|
||||
t.Logf("tokens +3min: %f", Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*3)))
|
||||
t.Logf("tokens +4min: %f", Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*4)))
|
||||
t.Logf("tokens +5min: %f", Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*5)))
|
||||
t.Logf("tokens +10min: %f", Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*10)))
|
||||
t.Logf("tokens +15min: %f", Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*15)))
|
||||
t.Logf("tokens +20min: %f", Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*20)))
|
||||
|
||||
assert.InEpsilon(t, 1, Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*1)), 0.1)
|
||||
assert.InEpsilon(t, 2, Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*2)), 0.1)
|
||||
assert.InEpsilon(t, 3, Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*3)), 0.1)
|
||||
assert.InEpsilon(t, 4, Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*4)), 0.1)
|
||||
assert.InEpsilon(t, 5, Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*5)), 0.1)
|
||||
assert.InEpsilon(t, 10, Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*10)), 0.1)
|
||||
assert.InEpsilon(t, 10, Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*20)), 0.01)
|
||||
|
||||
for i := 0; i < 30; i++ {
|
||||
assert.False(t, Auth.IP(clientIp).Allow())
|
||||
}
|
||||
|
||||
assert.False(t, Auth.IP(clientIp).Allow())
|
||||
|
||||
t.Logf("tokens now: %f", Auth.IP(clientIp).TokensAt(time.Now()))
|
||||
t.Logf("tokens +5min: %f", Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*5)))
|
||||
t.Logf("tokens +10min: %f", Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*10)))
|
||||
t.Logf("tokens +15min: %f", Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*15)))
|
||||
t.Logf("tokens +20min: %f", Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*20)))
|
||||
|
||||
assert.InEpsilon(t, 1, Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*1)), 0.1)
|
||||
assert.InEpsilon(t, 2, Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*2)), 0.1)
|
||||
assert.InEpsilon(t, 3, Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*3)), 0.1)
|
||||
assert.InEpsilon(t, 4, Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*4)), 0.1)
|
||||
assert.InEpsilon(t, 5, Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*5)), 0.1)
|
||||
assert.InEpsilon(t, 10, Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*10)), 0.1)
|
||||
assert.InEpsilon(t, 10, Auth.IP(clientIp).TokensAt(time.Now().Add(time.Minute*20)), 0.01)
|
||||
}
|
13
internal/server/limiter/login.go
Normal file
13
internal/server/limiter/login.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package limiter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
const DefaultLoginLimit = 10
|
||||
const DefaultLoginInterval = time.Minute
|
||||
|
||||
// Login limits failed authentication requests (one per minute).
|
||||
var Login = NewLimit(rate.Every(DefaultLoginInterval), DefaultLoginLimit)
|
60
internal/server/limiter/login_test.go
Normal file
60
internal/server/limiter/login_test.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package limiter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
clientIp := "192.0.2.42"
|
||||
|
||||
for i := 0; i < 9; i++ {
|
||||
t.Logf("tokens now: %f", Login.IP(clientIp).TokensAt(time.Now()))
|
||||
assert.True(t, Login.IP(clientIp).Allow())
|
||||
}
|
||||
|
||||
assert.True(t, Login.IP(clientIp).Allow())
|
||||
assert.False(t, Login.IP(clientIp).Allow())
|
||||
assert.False(t, Login.IP(clientIp).Allow())
|
||||
assert.False(t, Login.IP(clientIp).Allow())
|
||||
|
||||
t.Logf("tokens now: %f", Login.IP(clientIp).TokensAt(time.Now()))
|
||||
t.Logf("tokens +1min: %f", Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute)))
|
||||
t.Logf("tokens +2min: %f", Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*2)))
|
||||
t.Logf("tokens +3min: %f", Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*3)))
|
||||
t.Logf("tokens +4min: %f", Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*4)))
|
||||
t.Logf("tokens +5min: %f", Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*5)))
|
||||
t.Logf("tokens +10min: %f", Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*10)))
|
||||
t.Logf("tokens +15min: %f", Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*15)))
|
||||
t.Logf("tokens +20min: %f", Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*20)))
|
||||
|
||||
assert.InEpsilon(t, 1, Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*1)), 0.1)
|
||||
assert.InEpsilon(t, 2, Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*2)), 0.1)
|
||||
assert.InEpsilon(t, 3, Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*3)), 0.1)
|
||||
assert.InEpsilon(t, 4, Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*4)), 0.1)
|
||||
assert.InEpsilon(t, 5, Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*5)), 0.1)
|
||||
assert.InEpsilon(t, 10, Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*10)), 0.1)
|
||||
assert.InEpsilon(t, 10, Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*20)), 0.01)
|
||||
|
||||
for i := 0; i < 30; i++ {
|
||||
assert.False(t, Login.IP(clientIp).Allow())
|
||||
}
|
||||
|
||||
assert.False(t, Login.IP(clientIp).Allow())
|
||||
|
||||
t.Logf("tokens now: %f", Login.IP(clientIp).TokensAt(time.Now()))
|
||||
t.Logf("tokens +5min: %f", Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*5)))
|
||||
t.Logf("tokens +10min: %f", Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*10)))
|
||||
t.Logf("tokens +15min: %f", Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*15)))
|
||||
t.Logf("tokens +20min: %f", Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*20)))
|
||||
|
||||
assert.InEpsilon(t, 1, Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*1)), 0.1)
|
||||
assert.InEpsilon(t, 2, Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*2)), 0.1)
|
||||
assert.InEpsilon(t, 3, Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*3)), 0.1)
|
||||
assert.InEpsilon(t, 4, Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*4)), 0.1)
|
||||
assert.InEpsilon(t, 5, Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*5)), 0.1)
|
||||
assert.InEpsilon(t, 10, Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*10)), 0.1)
|
||||
assert.InEpsilon(t, 10, Login.IP(clientIp).TokensAt(time.Now().Add(time.Minute*20)), 0.01)
|
||||
}
|
|
@ -74,22 +74,21 @@ func Start(ctx context.Context, conf *config.Config) {
|
|||
|
||||
// Enable TLS?
|
||||
if tlsManager, tlsErr = AutoTLS(conf); tlsErr == nil {
|
||||
httpsRedirect = conf.HttpsRedirect()
|
||||
server = &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpsPort()),
|
||||
Addr: fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort()),
|
||||
TLSConfig: tlsManager.TLSConfig(),
|
||||
Handler: router,
|
||||
}
|
||||
log.Infof("server: starting in auto tls mode on %s [%s]", server.Addr, time.Since(start))
|
||||
go StartAutoTLS(server, tlsManager, conf)
|
||||
} else if httpsCert, privateKey := conf.TLS(); httpsCert != "" && privateKey != "" {
|
||||
} else if publicCert, privateKey := conf.TLS(); publicCert != "" && privateKey != "" {
|
||||
log.Infof("server: starting in manual tls mode")
|
||||
server = &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpsPort()),
|
||||
Addr: fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort()),
|
||||
Handler: router,
|
||||
}
|
||||
log.Infof("server: listening on %s [%s]", server.Addr, time.Since(start))
|
||||
go StartTLS(server, httpsCert, privateKey)
|
||||
go StartTLS(server, publicCert, privateKey)
|
||||
} else {
|
||||
log.Infof("server: %s", tlsErr)
|
||||
server = &http.Server{
|
||||
|
|
|
@ -23,6 +23,20 @@ func UniqueNames(names []string) (result []string) {
|
|||
return result
|
||||
}
|
||||
|
||||
// AppendName appends a name to an existing name.
|
||||
func AppendName(s, n string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
n = strings.TrimSpace(n)
|
||||
|
||||
if s == "" {
|
||||
return n
|
||||
} else if s == n {
|
||||
return s
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s", s, n)
|
||||
}
|
||||
|
||||
// JoinNames joins a list of names to be used in titles and descriptions.
|
||||
func JoinNames(names []string, shorten bool) (result string) {
|
||||
l := len(names)
|
||||
|
|
51
pkg/txt/names_lists.go
Normal file
51
pkg/txt/names_lists.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package txt
|
||||
|
||||
func init() {
|
||||
IsNameSuffix = make(map[string]bool, len(NameSuffixes))
|
||||
for _, n := range NameSuffixes {
|
||||
IsNameSuffix[n] = true
|
||||
}
|
||||
|
||||
IsNameTitle = make(map[string]bool, len(NameTitles))
|
||||
for _, n := range NameTitles {
|
||||
IsNameTitle[n] = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var IsNameSuffix map[string]bool
|
||||
var IsNameTitle map[string]bool
|
||||
|
||||
var NameSuffixes = []string{"esq", "esquire", "jr", "jnr", "sr", "snr", "2", "ii", "iii", "iv",
|
||||
"v", "clu", "chfc", "cfp", "md", "phd", "j.d.", "ll.m.", "m.d.", "d.o.", "d.c.",
|
||||
"p.c.", "ph.d."}
|
||||
|
||||
var NameTitles = []string{"mr", "mrs", "ms", "miss", "dr", "herr", "monsieur", "hr", "frau",
|
||||
"a v m", "admiraal", "admiral", "air cdre", "air commodore", "air marshal",
|
||||
"air vice marshal", "alderman", "alhaji", "ambassador", "baron", "barones",
|
||||
"brig", "brig gen", "brig general", "brigadier", "brigadier general",
|
||||
"brother", "canon", "capt", "captain", "cardinal", "cdr", "chief", "cik", "cmdr",
|
||||
"coach", "col", "colonel", "commandant", "commander", "commissioner",
|
||||
"commodore", "comte", "comtessa", "congressman", "conseiller", "consul",
|
||||
"conte", "contessa", "corporal", "councillor", "count", "countess",
|
||||
"crown prince", "crown princess", "dame", "datin", "dato", "datuk",
|
||||
"datuk seri", "deacon", "deaconess", "dean", "dhr", "dipl ing", "doctor",
|
||||
"dott", "dott sa", "dr ing", "dra", "drs", "embajador", "embajadora", "en",
|
||||
"encik", "eng", "eur ing", "exma sra", "exmo sr", "f o", "father",
|
||||
"first lieutient", "first officer", "flt lieut", "flying officer", "fr",
|
||||
"frau", "fraulein", "fru", "gen", "generaal", "general", "governor", "graaf",
|
||||
"gravin", "group captain", "grp capt", "h e dr", "h h", "h m", "h r h", "hajah",
|
||||
"haji", "hajim", "her highness", "her majesty", "herr", "high chief",
|
||||
"his highness", "his holiness", "his majesty", "hon", "hr", "hra", "ing", "ir",
|
||||
"jonkheer", "judge", "justice", "khun ying", "kolonel", "lady", "lcda", "lic",
|
||||
"lieut", "lieut cdr", "lieut col", "lieut gen", "lord", "m", "m l", "m r",
|
||||
"madame", "mademoiselle", "maj gen", "major", "master", "mevrouw", "miss",
|
||||
"mlle", "mme", "monsieur", "monsignor", "mstr", "nti", "pastor",
|
||||
"president", "prince", "princess", "princesse", "prinses", "prof",
|
||||
"prof sir", "professor", "puan", "puan sri", "rabbi", "rear admiral", "rev",
|
||||
"rev canon", "rev dr", "rev mother", "reverend", "rva", "senator", "sergeant",
|
||||
"sheikh", "sheikha", "sig", "sig na", "sig ra", "sir", "sister", "sqn ldr", "sr",
|
||||
"sr d", "sra", "srta", "sultan", "tan sri", "tan sri dato", "tengku", "teuku",
|
||||
"than puying", "the hon dr", "the hon justice", "the hon miss", "the hon mr",
|
||||
"the hon mrs", "the hon ms", "the hon sir", "the very rev", "toh puan", "tun",
|
||||
"vice admiral", "viscount", "viscountess", "wg cdr"}
|
51
pkg/txt/names_parser.go
Normal file
51
pkg/txt/names_parser.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package txt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Name represents the components of a full name.
|
||||
type Name struct {
|
||||
Title string
|
||||
Given string
|
||||
Middle string
|
||||
Family string
|
||||
Suffix string
|
||||
Nick string
|
||||
}
|
||||
|
||||
// ParseName parses a full name and returns the components.
|
||||
func ParseName(full string) Name {
|
||||
name := Name{}
|
||||
name.Parse(full)
|
||||
return name
|
||||
}
|
||||
|
||||
// Parse tries to parse a full name.
|
||||
func (name *Name) Parse(full string) {
|
||||
if full == "" {
|
||||
return
|
||||
}
|
||||
|
||||
for _, w := range KeywordsRegexp.FindAllString(full, -1) {
|
||||
w = strings.Trim(w, "- '")
|
||||
|
||||
if w == "" || len(w) < 2 && IsLatin(w) {
|
||||
continue
|
||||
}
|
||||
|
||||
l := strings.ToLower(w)
|
||||
|
||||
if IsNameTitle[l] {
|
||||
name.Title = AppendName(name.Title, w)
|
||||
} else if IsNameSuffix[l] {
|
||||
name.Suffix = AppendName(name.Suffix, w)
|
||||
} else if name.Given == "" {
|
||||
name.Given = w
|
||||
} else {
|
||||
name.Family = AppendName(name.Family, w)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
15
pkg/txt/names_parser_test.go
Normal file
15
pkg/txt/names_parser_test.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package txt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseName(t *testing.T) {
|
||||
t.Run("BillGates", func(t *testing.T) {
|
||||
result := ParseName("William Henry Gates III")
|
||||
t.Logf("Name: %#v", result)
|
||||
assert.Equal(t, "William", result.Given)
|
||||
})
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
sizes=(16 20 29 32 40 48 50 55 56 60 64 72 76 80 100 114 120 128 144 152 160 167 172 175 180 192 196 200 216 256 267 400 512 1024)
|
||||
|
||||
if [[ -z $1 ]] && [[ -z $2 ]]; then
|
||||
echo "Please provide a source SVG and target PNG name" 1>&2
|
||||
exit 1
|
||||
elif [[ $1 ]] && [[ -z $2 ]]; then
|
||||
mkdir -p "assets/static/icons/$1"
|
||||
|
||||
if [ -f "assets/static/icons/$1.svg" ]; then
|
||||
echo "creating png icons from assets/static/icons/$1.svg..."
|
||||
else
|
||||
echo "assets/static/icons/$1.svg not found"
|
||||
fi
|
||||
|
||||
for i in "${sizes[@]}"
|
||||
do
|
||||
rsvg-convert -a -w $i -h $i "assets/static/icons/$1.svg" > "assets/static/icons/$1/$i.png"
|
||||
echo "assets/static/icons/$1/$i.png"
|
||||
done
|
||||
else
|
||||
if [ -f "$1" ]; then
|
||||
echo "creating png icons from $1..."
|
||||
else
|
||||
echo "$1 not found"
|
||||
fi
|
||||
|
||||
for i in "${sizes[@]}"
|
||||
do
|
||||
rsvg-convert -a -w $i -h $i $1 > "$2-$i.png"
|
||||
echo "$2-$i.png"
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Done"
|
Loading…
Reference in a new issue