mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
API: Improve logs and add /api/v1/connect endpoint for auth callbacks
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -33,7 +33,8 @@ import Labels from "pages/labels.vue";
|
||||
import People from "pages/people.vue";
|
||||
import Library from "pages/library.vue";
|
||||
import Settings from "pages/settings.vue";
|
||||
import AuthLogin from "pages/auth/login.vue";
|
||||
import Login from "pages/auth/login.vue";
|
||||
import Connect from "pages/auth/connect.vue";
|
||||
import Discover from "pages/discover.vue";
|
||||
import About from "pages/about/about.vue";
|
||||
import Feedback from "pages/about/feedback.vue";
|
||||
@@ -78,7 +79,7 @@ export default [
|
||||
{
|
||||
name: "login",
|
||||
path: "/auth/login",
|
||||
component: AuthLogin,
|
||||
component: Login,
|
||||
meta: { title: siteTitle, auth: false, hideNav: true },
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (session.isUser()) {
|
||||
@@ -88,6 +89,17 @@ export default [
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "connect",
|
||||
path: "/connect/:name/:token",
|
||||
component: Connect,
|
||||
meta: {
|
||||
title: siteTitle,
|
||||
auth: true,
|
||||
admin: true,
|
||||
settings: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "browse",
|
||||
path: "/browse",
|
||||
|
||||
@@ -349,6 +349,7 @@ ol, ul {
|
||||
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 {
|
||||
border-radius: 6px;
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
.p-page-about {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.p-page .h30 {
|
||||
min-height: 30vh;
|
||||
}
|
||||
|
||||
.p-page .h40 {
|
||||
min-height: 40vh;
|
||||
}
|
||||
|
||||
.p-page .h50 {
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
.p-page .h60 {
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.p-page .h70 {
|
||||
min-height: 70vh;
|
||||
}
|
||||
121
frontend/src/pages/auth/connect.vue
Normal file
121
frontend/src/pages/auth/connect.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="p-page p-page-connect">
|
||||
<v-toolbar flat color="secondary" :dense="$vuetify.breakpoint.smAndDown">
|
||||
<v-toolbar-title>
|
||||
<a href="https://photoprism.app/membership" target="_blank">Membership</a> <v-icon>navigate_next</v-icon>
|
||||
<span v-if="busy">
|
||||
<translate>Busy, please wait…</translate>
|
||||
</span>
|
||||
<span v-else-if="success">
|
||||
<translate>Verified</translate>
|
||||
</span>
|
||||
<span v-else>
|
||||
<translate>Connect</translate>
|
||||
</span>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-shield" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"></path>
|
||||
</svg>
|
||||
</v-toolbar>
|
||||
<v-container fluid fill-height row wrap class="pa-4">
|
||||
<v-layout align-center justify-center fill-height>
|
||||
<v-flex v-if="busy" xs12 d-flex class="text-sm-center py-3">
|
||||
<v-progress-linear color="secondary-dark" :indeterminate="true"></v-progress-linear>
|
||||
</v-flex>
|
||||
<v-flex v-else-if="error" xs12 d-flex class="text-sm-left mb-3">
|
||||
<v-alert
|
||||
:value="true"
|
||||
color="error"
|
||||
icon="gpp_bad"
|
||||
class="mt-3"
|
||||
outline
|
||||
>
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
</v-flex>
|
||||
<v-flex v-else-if="success" xs12 d-flex class="text-sm-left mb-3">
|
||||
<v-alert
|
||||
:value="true"
|
||||
color="success"
|
||||
icon="verified_user"
|
||||
class="mt-3"
|
||||
outline
|
||||
>
|
||||
<translate>Successfully Connected</translate>
|
||||
</v-alert>
|
||||
</v-flex>
|
||||
<v-flex v-else xs12 d-flex class="text-sm-left mb-3">
|
||||
<v-alert
|
||||
:value="true"
|
||||
color="warning"
|
||||
icon="gpp_maybe"
|
||||
class="mt-3 ra-4"
|
||||
outline
|
||||
>
|
||||
<translate>Request failed - invalid response</translate>
|
||||
</v-alert>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
<p-about-footer></p-about-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as options from "options/options";
|
||||
import Api from "common/api";
|
||||
|
||||
export default {
|
||||
name: 'PPageConnect',
|
||||
data() {
|
||||
return {
|
||||
success: false,
|
||||
busy: false,
|
||||
valid: false,
|
||||
error: "",
|
||||
options: options,
|
||||
rtl: this.$rtl,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.$config.load().then(() => {
|
||||
this.send();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
send() {
|
||||
const name = this.$route.params.name.replace(/[^a-z0-9]/gi, '');
|
||||
const values = {Token: this.$route.params.token};
|
||||
|
||||
if (name !== '' && values.Token.length >= 4) {
|
||||
this.busy = true;
|
||||
this.$notify.blockUI();
|
||||
Api.put("connect/"+name, values).then(() => {
|
||||
this.$notify.success(this.$gettext("Connected"));
|
||||
this.success = true;
|
||||
this.busy = false;
|
||||
this.$config.update();
|
||||
}).catch((error) => {
|
||||
this.busy = false;
|
||||
if (error.response && error.response.data) {
|
||||
let data = error.response.data;
|
||||
this.error = data.message ? data.message : data.error;
|
||||
}
|
||||
|
||||
if(!this.error) {
|
||||
this.error = this.$gettext("Invalid parameters");
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$notify.unblockUI();
|
||||
});
|
||||
} else {
|
||||
this.$notify.error(this.$gettext("Invalid parameters"));
|
||||
this.$router.push({name: "settings"});
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -97,7 +97,7 @@
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "PPageAuthLogin",
|
||||
name: "PPageLogin",
|
||||
data() {
|
||||
const c = this.$config.values;
|
||||
const sponsor = this.$config.isSponsor();
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/internal/workers"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
@@ -89,7 +88,7 @@ func GetAccountFolders(router *gin.RouterGroup) {
|
||||
if cacheData, ok := cache.Get(cacheKey); ok {
|
||||
cached := cacheData.(fs.FileInfos)
|
||||
|
||||
log.Tracef("api: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
log.Tracef("api-v1: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
|
||||
c.JSON(http.StatusOK, cached)
|
||||
return
|
||||
|
||||
@@ -28,6 +28,7 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -60,7 +61,7 @@ func UpdateClientConfig() {
|
||||
func Abort(c *gin.Context, code int, id i18n.Message, params ...interface{}) {
|
||||
resp := i18n.NewResponse(code, id, params...)
|
||||
|
||||
log.Debugf("api: abort %s with code %d (%s)", clean.Log(c.FullPath()), code, resp.String())
|
||||
log.Debugf("api-v1: abort %s with code %d (%s)", clean.Log(c.FullPath()), code, strings.ToLower(resp.String()))
|
||||
|
||||
c.AbortWithStatusJSON(code, resp)
|
||||
}
|
||||
@@ -70,7 +71,7 @@ func Error(c *gin.Context, code int, err error, id i18n.Message, params ...inter
|
||||
|
||||
if err != nil {
|
||||
resp.Details = err.Error()
|
||||
log.Errorf("api: error %s with code %d in %s (%s)", clean.Log(err.Error()), code, clean.Log(c.FullPath()), resp.String())
|
||||
log.Errorf("api-v1: error %s with code %d in %s (%s)", clean.Log(err.Error()), code, clean.Log(c.FullPath()), strings.ToLower(resp.String()))
|
||||
}
|
||||
|
||||
c.AbortWithStatusJSON(code, resp)
|
||||
|
||||
74
internal/api/connect.go
Normal file
74
internal/api/connect.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
||||
// Connect confirms external service accounts using a token.
|
||||
//
|
||||
// PUT /api/v1/connect/:name
|
||||
func Connect(router *gin.RouterGroup) {
|
||||
router.PUT("/connect/:name", func(c *gin.Context) {
|
||||
name := clean.IdString(c.Param("name"))
|
||||
|
||||
if name == "" {
|
||||
log.Errorf("connect: empty service name")
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
var f form.Connect
|
||||
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
log.Warnf("connect: invalid form values (%s)", clean.Log(name))
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrAccountConnect)
|
||||
return
|
||||
}
|
||||
|
||||
if f.Invalid() {
|
||||
log.Warnf("connect: invalid token %s", clean.Log(f.Token))
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrAccountConnect)
|
||||
return
|
||||
}
|
||||
|
||||
conf := service.Config()
|
||||
|
||||
if conf.Public() {
|
||||
Abort(c, http.StatusForbidden, i18n.ErrPublic)
|
||||
return
|
||||
}
|
||||
|
||||
s := Auth(SessionID(c), acl.ResourceConfigOptions, acl.ActionUpdate)
|
||||
|
||||
if s.Invalid() {
|
||||
log.Errorf("connect: %s not authorized", clean.Log(s.User.UserName))
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
switch name {
|
||||
case "hub":
|
||||
err = conf.ResyncHub(f.Token)
|
||||
default:
|
||||
log.Errorf("connect: invalid service %s", clean.Log(name))
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrAccountConnect)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK})
|
||||
}
|
||||
})
|
||||
}
|
||||
46
internal/api/connect_test.go
Normal file
46
internal/api/connect_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConnect(t *testing.T) {
|
||||
t.Run("NoNameOrToken", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
Connect(router)
|
||||
r := PerformRequest(app, "PUT", "/api/v1/connect")
|
||||
assert.Equal(t, 404, r.Code)
|
||||
})
|
||||
t.Run("NoName", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
Connect(router)
|
||||
r := PerformRequest(app, "PUT", "/api/v1/connect/")
|
||||
assert.Equal(t, 404, r.Code)
|
||||
})
|
||||
t.Run("NoToken", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
Connect(router)
|
||||
r := PerformRequest(app, "PUT", "/api/v1/connect/hub/")
|
||||
assert.Equal(t, 307, r.Code)
|
||||
})
|
||||
t.Run("InvalidUrl", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
Connect(router)
|
||||
r := PerformRequest(app, "PUT", "/api/v1/connect/hub/a")
|
||||
assert.Equal(t, 404, r.Code)
|
||||
})
|
||||
t.Run("Redirect", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
Connect(router)
|
||||
r := PerformRequest(app, "PUT", "/api/v1/connect/hub/foobar123")
|
||||
assert.NotEqual(t, 301, r.Code)
|
||||
})
|
||||
t.Run("HasNameAndToken", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
Connect(router)
|
||||
r := PerformRequestWithBody(app, "PUT", "/api/v1/connect/hub/", `{"Token": "foobar123"}`)
|
||||
assert.NotEqual(t, 200, r.Code)
|
||||
})
|
||||
}
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/internal/thumb"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
@@ -53,7 +53,7 @@ func AlbumCover(router *gin.RouterGroup) {
|
||||
cacheKey := CacheKey(albumCover, uid, string(thumbName))
|
||||
|
||||
if cacheData, ok := cache.Get(cacheKey); ok {
|
||||
log.Tracef("api: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
log.Tracef("api-v1: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
|
||||
cached := cacheData.(ThumbCache)
|
||||
|
||||
@@ -165,7 +165,7 @@ func LabelCover(router *gin.RouterGroup) {
|
||||
cacheKey := CacheKey(labelCover, uid, string(thumbName))
|
||||
|
||||
if cacheData, ok := cache.Get(cacheKey); ok {
|
||||
log.Tracef("api: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
log.Tracef("api-v1: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
|
||||
cached := cacheData.(ThumbCache)
|
||||
|
||||
|
||||
@@ -3,13 +3,12 @@ package api
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
)
|
||||
|
||||
// SendFeedback sends a feedback message.
|
||||
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/internal/thumb"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
@@ -62,7 +62,7 @@ func FolderCover(router *gin.RouterGroup) {
|
||||
cacheKey := CacheKey(folderCover, uid, string(thumbName))
|
||||
|
||||
if cacheData, ok := cache.Get(cacheKey); ok {
|
||||
log.Tracef("api: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
log.Tracef("api-v1: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
|
||||
cached := cacheData.(ThumbCache)
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ func SearchFolders(router *gin.RouterGroup, urlPath, rootName, rootPath string)
|
||||
if cacheData, ok := cache.Get(cacheKey); ok {
|
||||
cached := cacheData.(FoldersResponse)
|
||||
|
||||
log.Tracef("api: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
log.Tracef("api-v1: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
|
||||
c.JSON(http.StatusOK, cached)
|
||||
return
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/internal/thumb"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
@@ -97,7 +96,7 @@ func GetThumb(router *gin.RouterGroup) {
|
||||
cacheKey := CacheKey("thumbs", fileHash, string(sizeName))
|
||||
|
||||
if cacheData, ok := cache.Get(cacheKey); ok {
|
||||
log.Tracef("api: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
log.Tracef("api-v1: cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||
|
||||
cached := cacheData.(ThumbCache)
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
// FacesCommand registers the facial recognition subcommands.
|
||||
// FacesCommand registers the face recognition subcommands.
|
||||
var FacesCommand = cli.Command{
|
||||
Name: "faces",
|
||||
Usage: "Facial recognition subcommands",
|
||||
Usage: "Face recognition subcommands",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "stats",
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/face"
|
||||
"github.com/photoprism/photoprism/internal/hub"
|
||||
"github.com/photoprism/photoprism/internal/hub/places"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/mutex"
|
||||
"github.com/photoprism/photoprism/internal/thumb"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
@@ -151,7 +152,7 @@ func (c *Config) Propagate() {
|
||||
places.UserAgent = c.UserAgent()
|
||||
entity.GeoApi = c.GeoApi()
|
||||
|
||||
// Set facial recognition parameters.
|
||||
// Set face recognition parameters.
|
||||
face.ScoreThreshold = c.FaceScore()
|
||||
face.OverlapThreshold = c.FaceOverlap()
|
||||
face.ClusterScoreThreshold = c.FaceClusterScore()
|
||||
@@ -672,15 +673,25 @@ func (c *Config) ResolutionLimit() int {
|
||||
return result
|
||||
}
|
||||
|
||||
// UpdateHub updates backend api credentials for maps & places.
|
||||
// UpdateHub renews backend api credentials for maps & places without a token.
|
||||
func (c *Config) UpdateHub() {
|
||||
if err := c.hub.Refresh(); err != nil {
|
||||
log.Debugf("config: %s", err)
|
||||
} else if err := c.hub.Save(); err != nil {
|
||||
log.Debugf("config: %s", err)
|
||||
_ = c.ResyncHub("")
|
||||
}
|
||||
|
||||
// ResyncHub renews backend api credentials for maps & places with an optional token.
|
||||
func (c *Config) ResyncHub(token string) error {
|
||||
if err := c.hub.Resync(token); err != nil {
|
||||
log.Debugf("config: %s (refresh backend api tokens)", err)
|
||||
if token != "" {
|
||||
return i18n.Error(i18n.ErrAccountConnect)
|
||||
}
|
||||
} else if err = c.hub.Save(); err != nil {
|
||||
log.Debugf("config: %s (save backend api tokens)", err)
|
||||
} else {
|
||||
c.hub.Propagate()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initHub initializes PhotoPrism hub config.
|
||||
@@ -693,10 +704,10 @@ func (c *Config) initHub() {
|
||||
|
||||
if err := c.hub.Load(); err == nil {
|
||||
// Do nothing.
|
||||
} else if err := c.hub.Refresh(); err != nil {
|
||||
log.Debugf("config: %s", err)
|
||||
} else if err := c.hub.Update(); err != nil {
|
||||
log.Debugf("config: %s (init backend api tokens)", err)
|
||||
} else if err := c.hub.Save(); err != nil {
|
||||
log.Debugf("config: %s", err)
|
||||
log.Debugf("config: %s (save backend api tokens)", err)
|
||||
}
|
||||
|
||||
c.hub.Propagate()
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
const (
|
||||
AuthModePublic = "public"
|
||||
AuthModePasswd = "passwd"
|
||||
AuthModePasswd = "password"
|
||||
)
|
||||
|
||||
func isBcrypt(s string) bool {
|
||||
@@ -53,7 +53,7 @@ func (c *Config) AuthMode() string {
|
||||
switch mode {
|
||||
case AuthModePublic:
|
||||
return AuthModePublic
|
||||
case "", "pw", "pass", "passwd", "password", "passwort", "passwords":
|
||||
case AuthModePasswd, "", "p", "pw", "pass", "passwd", "passwort", "passwords":
|
||||
return AuthModePasswd
|
||||
default:
|
||||
return AuthModePasswd
|
||||
|
||||
@@ -50,7 +50,7 @@ func (c *Config) DisableTensorFlow() bool {
|
||||
return c.options.DisableTensorFlow
|
||||
}
|
||||
|
||||
// DisableFaces checks if facial recognition is disabled.
|
||||
// DisableFaces checks if face recognition is disabled.
|
||||
func (c *Config) DisableFaces() bool {
|
||||
if c.DisableTensorFlow() || c.options.DisableFaces {
|
||||
return true
|
||||
|
||||
@@ -238,7 +238,7 @@ var Flags = CliFlags{
|
||||
CliFlag{
|
||||
Flag: cli.BoolFlag{
|
||||
Name: "disable-faces",
|
||||
Usage: "disable facial recognition",
|
||||
Usage: "disable face recognition",
|
||||
EnvVar: "PHOTOPRISM_DISABLE_FACES",
|
||||
}},
|
||||
CliFlag{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
|
||||
Package face provides facial recognition.
|
||||
Package face provides face recognition.
|
||||
|
||||
Copyright (c) 2018 - 2022 PhotoPrism UG. All rights reserved.
|
||||
|
||||
|
||||
15
internal/form/connect.go
Normal file
15
internal/form/connect.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// Connect represents a connection request with an external service.
|
||||
type Connect struct {
|
||||
Token string `json:"Token"`
|
||||
}
|
||||
|
||||
// Invalid tests if the form data is invalid.
|
||||
func (f Connect) Invalid() bool {
|
||||
return !rnd.ValidateCrcToken(f.Token)
|
||||
}
|
||||
22
internal/form/connect_test.go
Normal file
22
internal/form/connect_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConnect_Invalid(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
f := Connect{}
|
||||
assert.True(t, f.Invalid())
|
||||
})
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
f := Connect{Token: "1mna-2a4t-8729"}
|
||||
assert.True(t, f.Invalid())
|
||||
})
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
f := Connect{Token: "q85v-196o-7eb4"}
|
||||
assert.False(t, f.Invalid())
|
||||
})
|
||||
}
|
||||
@@ -8,18 +8,18 @@ import (
|
||||
|
||||
func TestFeedback_Empty(t *testing.T) {
|
||||
t.Run("true", func(t *testing.T) {
|
||||
feedback := Feedback{}
|
||||
assert.True(t, feedback.Empty())
|
||||
f := Feedback{}
|
||||
assert.True(t, f.Empty())
|
||||
})
|
||||
t.Run("false", func(t *testing.T) {
|
||||
feedback := Feedback{Message: "I found a bug", Category: "Bug Report", UserEmail: "test@test.com"}
|
||||
assert.False(t, feedback.Empty())
|
||||
f := Feedback{Message: "I found a bug", Category: "Bug Report", UserEmail: "test@test.com"}
|
||||
assert.False(t, f.Empty())
|
||||
})
|
||||
t.Run("false", func(t *testing.T) {
|
||||
feedback, err := NewFeedback("")
|
||||
if err != nil {
|
||||
if f, err := NewFeedback(""); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.True(t, f.Empty())
|
||||
}
|
||||
assert.True(t, feedback.Empty())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -143,8 +143,13 @@ func (c *Config) DecodeSession() (Session, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Refresh updates backend api credentials.
|
||||
func (c *Config) Refresh() (err error) {
|
||||
// Update renews backend api credentials without a token.
|
||||
func (c *Config) Update() error {
|
||||
return c.Resync("")
|
||||
}
|
||||
|
||||
// Resync renews backend api credentials with an optional token.
|
||||
func (c *Config) Resync(token string) (err error) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
@@ -176,8 +181,10 @@ func (c *Config) Refresh() (err error) {
|
||||
log.Debugf("config: requesting new api key for maps and places")
|
||||
}
|
||||
|
||||
// Create request.
|
||||
if j, err := json.Marshal(NewRequest(c.Version, c.Serial, c.Env, c.PartnerID)); err != nil {
|
||||
// Create JSON request.
|
||||
var j []byte
|
||||
|
||||
if j, err = json.Marshal(NewRequest(c.Version, c.Serial, c.Env, c.PartnerID, token)); err != nil {
|
||||
return err
|
||||
} else if req, err = http.NewRequest(method, url, bytes.NewReader(j)); err != nil {
|
||||
return err
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestNewConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewRequest(t *testing.T) {
|
||||
r := NewRequest("0.0.0", "zqkunt22r0bewti9", "test", "test")
|
||||
r := NewRequest("0.0.0", "zqkunt22r0bewti9", "test", "test", "")
|
||||
|
||||
assert.IsType(t, &Request{}, r)
|
||||
|
||||
@@ -74,7 +74,7 @@ func TestConfig_Refresh(t *testing.T) {
|
||||
|
||||
c := NewConfig("0.0.0", fileName, "zqkunt22r0bewti9", "test", "PhotoPrism/Test", "test")
|
||||
|
||||
if err := c.Refresh(); err != nil {
|
||||
if err := c.Update(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ func TestConfig_Refresh(t *testing.T) {
|
||||
|
||||
assert.FileExists(t, fileName)
|
||||
|
||||
if err := c.Refresh(); err != nil {
|
||||
if err := c.Update(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,11 @@ type Request struct {
|
||||
ClientCPU int `json:"ClientCPU"`
|
||||
ClientEnv string `json:"ClientEnv"`
|
||||
PartnerID string `json:"PartnerID"`
|
||||
ApiToken string `json:"ApiToken"`
|
||||
}
|
||||
|
||||
// NewRequest creates a new backend key request instance.
|
||||
func NewRequest(version, serial, env, partnerId string) *Request {
|
||||
func NewRequest(version, serial, env, partnerId, token string) *Request {
|
||||
return &Request{
|
||||
ClientVersion: version,
|
||||
ClientSerial: serial,
|
||||
@@ -28,6 +29,7 @@ func NewRequest(version, serial, env, partnerId string) *Request {
|
||||
ClientCPU: runtime.NumCPU(),
|
||||
ClientEnv: env,
|
||||
PartnerID: partnerId,
|
||||
ApiToken: token,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,5 +57,5 @@ func Msg(id Message, params ...interface{}) string {
|
||||
}
|
||||
|
||||
func Error(id Message, params ...interface{}) error {
|
||||
return errors.New(strings.ToLower(Msg(id, params...)))
|
||||
return errors.New(Msg(id, params...))
|
||||
}
|
||||
|
||||
@@ -58,20 +58,20 @@ func TestMsg(t *testing.T) {
|
||||
func TestError(t *testing.T) {
|
||||
t.Run("already exists", func(t *testing.T) {
|
||||
err := Error(ErrAlreadyExists, "A cat")
|
||||
assert.EqualError(t, err, "a cat already exists")
|
||||
assert.EqualError(t, err, "A cat already exists")
|
||||
})
|
||||
|
||||
t.Run("unexpected error", func(t *testing.T) {
|
||||
err := Error(ErrUnexpected, "A cat")
|
||||
assert.EqualError(t, err, "unexpected error, please try again")
|
||||
assert.EqualError(t, err, "Unexpected error, please try again")
|
||||
})
|
||||
|
||||
t.Run("already exists german", func(t *testing.T) {
|
||||
SetLocale("de")
|
||||
errGerman := Error(ErrAlreadyExists, "Eine Katze")
|
||||
assert.EqualError(t, errGerman, "eine katze existiert bereits")
|
||||
assert.EqualError(t, errGerman, "Eine Katze existiert bereits")
|
||||
SetLocale("")
|
||||
errDefault := Error(ErrAlreadyExists, "A cat")
|
||||
assert.EqualError(t, errDefault, "a cat already exists")
|
||||
assert.EqualError(t, errDefault, "A cat already exists")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ const (
|
||||
ErrInvalidName
|
||||
ErrBusy
|
||||
ErrWakeupInterval
|
||||
ErrAccountConnect
|
||||
|
||||
MsgChangesSaved
|
||||
MsgAlbumCreated
|
||||
@@ -117,6 +118,7 @@ var Messages = MessageMap{
|
||||
ErrInvalidName: gettext("Invalid name"),
|
||||
ErrBusy: gettext("Busy, please try again later"),
|
||||
ErrWakeupInterval: gettext("The wakeup interval is %s, but must be 1h or less"),
|
||||
ErrAccountConnect: gettext("Your account could not be connected"),
|
||||
|
||||
// Info and confirmation messages:
|
||||
MsgChangesSaved: gettext("Changes successfully saved"),
|
||||
|
||||
@@ -44,7 +44,7 @@ func (w *Faces) Start(opt FacesOptions) (err error) {
|
||||
}()
|
||||
|
||||
if w.Disabled() {
|
||||
return fmt.Errorf("facial recognition is disabled")
|
||||
return fmt.Errorf("face recognition is disabled")
|
||||
}
|
||||
|
||||
if err := mutex.FacesWorker.Start(); err != nil {
|
||||
@@ -161,7 +161,7 @@ func (w *Faces) Canceled() bool {
|
||||
return mutex.FacesWorker.Canceled() || mutex.MainWorker.Canceled() || mutex.MetaWorker.Canceled()
|
||||
}
|
||||
|
||||
// Disabled tests if facial recognition is disabled.
|
||||
// Disabled tests if face recognition is disabled.
|
||||
func (w *Faces) Disabled() bool {
|
||||
return w.conf.DisableFaces()
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// Cluster clusters indexed face embeddings.
|
||||
func (w *Faces) Cluster(opt FacesOptions) (added entity.Faces, err error) {
|
||||
if w.Disabled() {
|
||||
return added, fmt.Errorf("facial recognition is disabled")
|
||||
return added, fmt.Errorf("face recognition is disabled")
|
||||
}
|
||||
|
||||
// Skip clustering if index contains no new face markers, and force option isn't set.
|
||||
|
||||
@@ -27,7 +27,7 @@ func (r *FacesMatchResult) Add(result FacesMatchResult) {
|
||||
// Match matches markers with faces and subjects.
|
||||
func (w *Faces) Match(opt FacesOptions) (result FacesMatchResult, err error) {
|
||||
if w.Disabled() {
|
||||
return result, fmt.Errorf("facial recognition is disabled")
|
||||
return result, fmt.Errorf("face recognition is disabled")
|
||||
}
|
||||
|
||||
var unmatchedMarkers int
|
||||
|
||||
@@ -16,7 +16,7 @@ type FacesOptimizeResult struct {
|
||||
// Optimize optimizes the face lookup table.
|
||||
func (w *Faces) Optimize() (result FacesOptimizeResult, err error) {
|
||||
if w.Disabled() {
|
||||
return result, fmt.Errorf("facial recognition is disabled")
|
||||
return result, fmt.Errorf("face recognition is disabled")
|
||||
}
|
||||
|
||||
// Iterative merging of manually added face clusters.
|
||||
|
||||
@@ -247,9 +247,9 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
|
||||
}
|
||||
|
||||
if filesImported > 0 {
|
||||
// Run facial recognition if enabled.
|
||||
// Run face recognition if enabled.
|
||||
if w := NewFaces(imp.conf); w.Disabled() {
|
||||
log.Debugf("import: skipping facial recognition")
|
||||
log.Debugf("import: skipping face recognition")
|
||||
} else if err := w.Start(FacesOptionsDefault()); err != nil {
|
||||
log.Errorf("import: %s", err)
|
||||
}
|
||||
|
||||
@@ -261,9 +261,9 @@ func (ind *Index) Start(o IndexOptions) fs.Done {
|
||||
"step": "faces",
|
||||
})
|
||||
|
||||
// Run facial recognition if enabled.
|
||||
// Run face recognition if enabled.
|
||||
if w := NewFaces(ind.conf); w.Disabled() {
|
||||
log.Debugf("index: skipping facial recognition")
|
||||
log.Debugf("index: skipping face recognition")
|
||||
} else if err := w.Start(FacesOptionsDefault()); err != nil {
|
||||
log.Errorf("index: %s", err)
|
||||
}
|
||||
|
||||
@@ -396,7 +396,7 @@ func PhotosGeo(f form.SearchPhotosGeo) (results GeoResults, err error) {
|
||||
return results, result.Error
|
||||
}
|
||||
|
||||
log.Debugf("geo: found %s for %s [%s]", english.Plural(len(results), "result", "results"), f.SerializeAll(), time.Since(start))
|
||||
log.Debugf("places: found %s for %s [%s]", english.Plural(len(results), "result", "results"), f.SerializeAll(), time.Since(start))
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -171,6 +171,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
||||
api.GetErrors(v1)
|
||||
api.DeleteErrors(v1)
|
||||
api.SendFeedback(v1)
|
||||
api.Connect(v1)
|
||||
api.Websocket(v1)
|
||||
}
|
||||
|
||||
|
||||
@@ -44,11 +44,11 @@ func (m *Meta) Start(delay, interval time.Duration, force bool) (err error) {
|
||||
|
||||
defer mutex.MetaWorker.Stop()
|
||||
|
||||
log.Debugf("metadata: running facial recognition")
|
||||
log.Debugf("metadata: running face recognition")
|
||||
|
||||
// Run faces worker.
|
||||
if w := photoprism.NewFaces(m.conf); w.Disabled() {
|
||||
log.Debugf("metadata: skipping facial recognition")
|
||||
log.Debugf("metadata: skipping face recognition")
|
||||
} else if err := w.Start(photoprism.FacesOptions{}); err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
|
||||
37
pkg/rnd/crc_token.go
Normal file
37
pkg/rnd/crc_token.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package rnd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// CrcToken returns a string token with checksum.
|
||||
func CrcToken() string {
|
||||
token := make([]byte, 0, 14)
|
||||
|
||||
token = append(token, []byte(GenerateToken(4))...)
|
||||
token = append(token, '-')
|
||||
token = append(token, []byte(GenerateToken(4))...)
|
||||
|
||||
checksum := crc32.ChecksumIEEE(token)
|
||||
|
||||
sum := strconv.FormatInt(int64(checksum), 16)
|
||||
|
||||
return fmt.Sprintf("%s-%.4s", token, sum)
|
||||
}
|
||||
|
||||
// ValidateCrcToken tests if the token string is valid.
|
||||
func ValidateCrcToken(s string) bool {
|
||||
if len(s) != 14 {
|
||||
return false
|
||||
}
|
||||
|
||||
token := []byte(s[:9])
|
||||
|
||||
checksum := crc32.ChecksumIEEE(token)
|
||||
|
||||
sum := strconv.FormatInt(int64(checksum), 16)
|
||||
|
||||
return s == fmt.Sprintf("%s-%.4s", token, sum)
|
||||
}
|
||||
33
pkg/rnd/crc_token_test.go
Normal file
33
pkg/rnd/crc_token_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package rnd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCrcToken(t *testing.T) {
|
||||
t.Run("size 4", func(t *testing.T) {
|
||||
token := CrcToken()
|
||||
t.Logf("CrcToken: %s", token)
|
||||
assert.NotEmpty(t, token)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateCrcToken(t *testing.T) {
|
||||
t.Run("Static", func(t *testing.T) {
|
||||
assert.True(t, ValidateCrcToken("8yva-2ni4-385e"))
|
||||
})
|
||||
t.Run("Generated", func(t *testing.T) {
|
||||
assert.True(t, ValidateCrcToken(CrcToken()))
|
||||
})
|
||||
t.Run("InvalidChecksum", func(t *testing.T) {
|
||||
assert.False(t, ValidateCrcToken("8yva-2ni4-185e"))
|
||||
})
|
||||
t.Run("InvalidToken", func(t *testing.T) {
|
||||
assert.False(t, ValidateCrcToken("8ava-2ni4-385e"))
|
||||
})
|
||||
t.Run("ValidToken", func(t *testing.T) {
|
||||
assert.True(t, ValidateCrcToken("hsqq-181q-13ed"))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user