diff --git a/graphql/documents/queries/settings/config.graphql b/graphql/documents/queries/settings/config.graphql index 4ee9d4ec6..0a4b076d2 100644 --- a/graphql/documents/queries/settings/config.graphql +++ b/graphql/documents/queries/settings/config.graphql @@ -11,3 +11,10 @@ query Directory($path: String) { directories } } + +query ValidateStashBox($input: StashBoxInput!) { + validateStashBoxCredentials(input: $input) { + valid + status + } +} diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index 3f6419fed..044dbf49a 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -136,6 +136,7 @@ type Query { "Desired collation locale. Determines the order of the directory result. eg. 'en-US', 'pt-BR', ..." locale: String = "en" ): Directory! + validateStashBoxCredentials(input: StashBoxInput!): StashBoxValidationResult! # System status systemStatus: SystemStatus! diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 0bab8fd93..d4fe5aa31 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -391,3 +391,8 @@ type StashConfig { input GenerateAPIKeyInput { clear: Boolean } + +type StashBoxValidationResult { + valid: Boolean! + status: String! +} diff --git a/graphql/stash-box/query.graphql b/graphql/stash-box/query.graphql index 9bc24f70a..12db15ca5 100644 --- a/graphql/stash-box/query.graphql +++ b/graphql/stash-box/query.graphql @@ -156,3 +156,9 @@ query FindSceneByID($id: ID!) { mutation SubmitFingerprint($input: FingerprintSubmission!) { submitFingerprint(input: $input) } + +query Me { + me { + name + } +} diff --git a/pkg/api/resolver_query_configuration.go b/pkg/api/resolver_query_configuration.go index b54019b4c..771c32357 100644 --- a/pkg/api/resolver_query_configuration.go +++ b/pkg/api/resolver_query_configuration.go @@ -2,9 +2,12 @@ package api import ( "context" + "fmt" + "strings" "github.com/stashapp/stash/pkg/manager/config" "github.com/stashapp/stash/pkg/models" + "github.com/stashapp/stash/pkg/scraper/stashbox" "github.com/stashapp/stash/pkg/utils" "golang.org/x/text/collate" ) @@ -188,3 +191,38 @@ func makeConfigDefaultsResult() *models.ConfigDefaultSettingsResult { DeleteGenerated: &deleteGeneratedDefault, } } + +func (r *queryResolver) ValidateStashBoxCredentials(ctx context.Context, input models.StashBoxInput) (*models.StashBoxValidationResult, error) { + client := stashbox.NewClient(models.StashBox{Endpoint: input.Endpoint, APIKey: input.APIKey}, r.txnManager) + user, err := client.GetUser(ctx) + + valid := user != nil && user.Me != nil + var status string + if valid { + status = fmt.Sprintf("Successfully authenticated as %s", user.Me.Name) + } else { + switch { + case strings.Contains(strings.ToLower(err.Error()), "doctype"): + // Index file returned rather than graphql + status = "Invalid endpoint" + case strings.Contains(err.Error(), "request failed"): + status = "No response from server" + case strings.HasPrefix(err.Error(), "invalid character") || + strings.HasPrefix(err.Error(), "illegal base64 data") || + err.Error() == "unexpected end of JSON input" || + err.Error() == "token contains an invalid number of segments": + status = "Malformed API key." + case err.Error() == "" || err.Error() == "signature is invalid": + status = "Invalid or expired API key." + default: + status = fmt.Sprintf("Unknown error: %s", err) + } + } + + result := models.StashBoxValidationResult{ + Valid: valid, + Status: status, + } + + return &result, nil +} diff --git a/pkg/scraper/stashbox/graphql/generated_client.go b/pkg/scraper/stashbox/graphql/generated_client.go index 8e0b31429..fd48d6528 100644 --- a/pkg/scraper/stashbox/graphql/generated_client.go +++ b/pkg/scraper/stashbox/graphql/generated_client.go @@ -180,45 +180,32 @@ type FindSceneByID struct { type SubmitFingerprintPayload struct { SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\"" } +type Me struct { + Me *struct { + Name string "json:\"name\" graphql:\"name\"" + } "json:\"me\" graphql:\"me\"" +} const FindSceneByFingerprintQuery = `query FindSceneByFingerprint ($fingerprint: FingerprintQueryInput!) { findSceneByFingerprint(fingerprint: $fingerprint) { ... SceneFragment } } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment SceneFragment on Scene { - id - title - details +fragment FingerprintFragment on Fingerprint { + algorithm + hash duration - date - urls { - ... URLFragment - } - images { - ... ImageFragment - } - studio { - ... StudioFragment - } - tags { - ... TagFragment - } - performers { - ... PerformerAppearanceFragment - } - fingerprints { - ... FingerprintFragment - } } fragment URLFragment on URL { url type } +fragment ImageFragment on Image { + id + url + width + height +} fragment StudioFragment on Studio { name id @@ -269,31 +256,49 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } -fragment ImageFragment on Image { +fragment BodyModificationFragment on BodyModification { + location + description +} +fragment SceneFragment on Scene { id - url - width - height + title + details + duration + date + urls { + ... URLFragment + } + images { + ... ImageFragment + } + studio { + ... StudioFragment + } + tags { + ... TagFragment + } + performers { + ... PerformerAppearanceFragment + } + fingerprints { + ... FingerprintFragment + } } fragment TagFragment on Tag { name id } +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} fragment MeasurementsFragment on Measurements { band_size cup_size waist hip } -fragment BodyModificationFragment on BodyModification { - location - description -} -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} ` func (c *Client) FindSceneByFingerprint(ctx context.Context, fingerprint FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByFingerprint, error) { @@ -314,12 +319,6 @@ const FindScenesByFullFingerprintsQuery = `query FindScenesByFullFingerprints ($ ... SceneFragment } } -fragment ImageFragment on Image { - id - url - width - height -} fragment StudioFragment on Studio { name id @@ -330,10 +329,6 @@ fragment StudioFragment on Studio { ... ImageFragment } } -fragment TagFragment on Tag { - name - id -} fragment PerformerFragment on Performer { id name @@ -372,11 +367,14 @@ fragment FuzzyDateFragment on FuzzyDate { date accuracy } -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip +fragment BodyModificationFragment on BodyModification { + location + description +} +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration } fragment SceneFragment on Scene { id @@ -403,24 +401,31 @@ fragment SceneFragment on Scene { ... FingerprintFragment } } +fragment URLFragment on URL { + url + type +} +fragment ImageFragment on Image { + id + url + width + height +} +fragment TagFragment on Tag { + name + id +} fragment PerformerAppearanceFragment on PerformerAppearance { as performer { ... PerformerFragment } } -fragment BodyModificationFragment on BodyModification { - location - description -} -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} -fragment URLFragment on URL { - url - type +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip } ` @@ -452,18 +457,19 @@ fragment ImageFragment on Image { width height } -fragment TagFragment on Tag { - name - id -} fragment FuzzyDateFragment on FuzzyDate { date accuracy } -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} +fragment BodyModificationFragment on BodyModification { + location + description } fragment SceneFragment on Scene { id @@ -500,6 +506,10 @@ fragment StudioFragment on Studio { ... ImageFragment } } +fragment TagFragment on Tag { + name + id +} fragment PerformerAppearanceFragment on PerformerAppearance { as performer { @@ -540,15 +550,10 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment BodyModificationFragment on BodyModification { - location - description +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration } ` @@ -570,30 +575,6 @@ const SearchPerformerQuery = `query SearchPerformer ($term: String!) { ... PerformerFragment } } -fragment URLFragment on URL { - url - type -} -fragment ImageFragment on Image { - id - url - width - height -} -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment BodyModificationFragment on BodyModification { - location - description -} fragment PerformerFragment on Performer { id name @@ -628,6 +609,30 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } +fragment URLFragment on URL { + url + type +} +fragment ImageFragment on Image { + id + url + width + height +} +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} +fragment BodyModificationFragment on BodyModification { + location + description +} ` func (c *Client) SearchPerformer(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchPerformer, error) { @@ -732,6 +737,35 @@ fragment ImageFragment on Image { width height } +fragment TagFragment on Tag { + name + id +} +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration +} +fragment URLFragment on URL { + url + type +} fragment StudioFragment on Studio { name id @@ -742,10 +776,6 @@ fragment StudioFragment on Studio { ... ImageFragment } } -fragment TagFragment on Tag { - name - id -} fragment PerformerFragment on Performer { id name @@ -780,16 +810,9 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration +fragment BodyModificationFragment on BodyModification { + location + description } fragment SceneFragment on Scene { id @@ -816,24 +839,6 @@ fragment SceneFragment on Scene { ... FingerprintFragment } } -fragment URLFragment on URL { - url - type -} -fragment BodyModificationFragment on BodyModification { - location - description -} -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} ` func (c *Client) FindSceneByID(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByID, error) { @@ -866,3 +871,21 @@ func (c *Client) SubmitFingerprint(ctx context.Context, input FingerprintSubmiss return &res, nil } + +const MeQuery = `query Me { + me { + name + } +} +` + +func (c *Client) Me(ctx context.Context, httpRequestOptions ...client.HTTPRequestOption) (*Me, error) { + vars := map[string]interface{}{} + + var res Me + if err := c.Client.Post(ctx, MeQuery, &res, vars, httpRequestOptions...); err != nil { + return nil, err + } + + return &res, nil +} diff --git a/pkg/scraper/stashbox/stash_box.go b/pkg/scraper/stashbox/stash_box.go index d8c817b6a..2ffefa2ff 100644 --- a/pkg/scraper/stashbox/stash_box.go +++ b/pkg/scraper/stashbox/stash_box.go @@ -753,3 +753,7 @@ func (c Client) FindStashBoxPerformerByName(ctx context.Context, name string) (* return ret, nil } + +func (c Client) GetUser(ctx context.Context) (*graphql.Me, error) { + return c.client.Me(ctx) +} diff --git a/ui/v2.5/src/components/Changelog/versions/v0130.md b/ui/v2.5/src/components/Changelog/versions/v0130.md index 90bb459b4..52a440c56 100644 --- a/ui/v2.5/src/components/Changelog/versions/v0130.md +++ b/ui/v2.5/src/components/Changelog/versions/v0130.md @@ -1,5 +1,6 @@ ### 🎨 Improvements -Show counts on list tabs in Performer, Studio and Tag pages. ([#2169](https://github.com/stashapp/stash/pull/2169)) +* Add button to test credentials when adding/editing stash-box endpoints. ([#2173](https://github.com/stashapp/stash/pull/2173)) +* Show counts on list tabs in Performer, Studio and Tag pages. ([#2169](https://github.com/stashapp/stash/pull/2169)) ### 🐛 Bug fixes * Generate sprites for short video files. ([#2167](https://github.com/stashapp/stash/pull/2167)) diff --git a/ui/v2.5/src/components/Settings/StashBoxConfiguration.tsx b/ui/v2.5/src/components/Settings/StashBoxConfiguration.tsx index 5adcabd29..9d0c4cfd1 100644 --- a/ui/v2.5/src/components/Settings/StashBoxConfiguration.tsx +++ b/ui/v2.5/src/components/Settings/StashBoxConfiguration.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useRef, useState } from "react"; import { Button, Form } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; import { SettingSection } from "./SettingSection"; @@ -12,6 +12,24 @@ export interface IStashBoxModal { export const StashBoxModal: React.FC = ({ value, close }) => { const intl = useIntl(); + const endpoint = useRef(null); + const apiKey = useRef(null); + + const [validate, { data, loading }] = GQL.useValidateStashBoxLazyQuery({ + fetchPolicy: "network-only", + }); + + const handleValidate = () => { + validate({ + variables: { + input: { + endpoint: endpoint.current?.value ?? "", + api_key: apiKey.current?.value ?? "", + name: "test", + }, + }, + }); + }; return ( @@ -52,6 +70,7 @@ export const StashBoxModal: React.FC = ({ value, close }) => { onChange={(e: React.ChangeEvent) => setValue({ ...v!, endpoint: e.currentTarget.value.trim() }) } + ref={endpoint} /> @@ -71,8 +90,30 @@ export const StashBoxModal: React.FC = ({ value, close }) => { onChange={(e: React.ChangeEvent) => setValue({ ...v!, api_key: e.currentTarget.value.trim() }) } + ref={apiKey} /> + + + + {data && ( + + {data.validateStashBoxCredentials?.status} + + )} + )} close={close}