2021-01-31 09:08:52 +01:00
|
|
|
package photoprism
|
2021-01-31 07:41:23 +01:00
|
|
|
|
2021-02-04 06:01:21 +01:00
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2021-02-09 20:17:06 +01:00
|
|
|
"io/ioutil"
|
2021-02-04 06:01:21 +01:00
|
|
|
"net/http"
|
|
|
|
"strings"
|
2021-01-31 09:08:52 +01:00
|
|
|
|
2021-02-04 06:01:21 +01:00
|
|
|
"github.com/kris-nova/client-go/api/v1"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// DefaultContentType is the content type header the API expects
|
|
|
|
DefaultContentType string = "application/json; charset=utf-8"
|
|
|
|
|
|
|
|
// Default Host Configuration
|
|
|
|
DefaultHost string = "localhost"
|
|
|
|
DefaultLoopback string = "127.0.0.1"
|
|
|
|
DefaultPort string = "8080"
|
|
|
|
DefaultConnectionString string = "http://localhost:8080"
|
2021-02-09 20:17:06 +01:00
|
|
|
DefaultTokenKey string = "X-Session-Id"
|
2021-02-04 06:01:21 +01:00
|
|
|
)
|
|
|
|
|
2021-02-09 20:17:06 +01:00
|
|
|
func New(connectionString string) *Client {
|
|
|
|
c := &Client{
|
2021-02-04 06:01:21 +01:00
|
|
|
contentType: DefaultContentType,
|
|
|
|
connectionString: DefaultConnectionString,
|
|
|
|
}
|
2021-02-09 20:17:06 +01:00
|
|
|
return c
|
2021-02-04 06:01:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Client represents a client to a Photoprism application
|
2021-01-31 09:08:52 +01:00
|
|
|
type Client struct {
|
2021-02-04 06:01:21 +01:00
|
|
|
v1client *api.V1Client
|
|
|
|
authenticator ClientAuthenticator
|
|
|
|
contentType string
|
|
|
|
connectionString string
|
2021-01-31 09:08:52 +01:00
|
|
|
}
|
|
|
|
|
2021-02-04 06:01:21 +01:00
|
|
|
// ClientAuthenticator is used to store the secret
|
|
|
|
// data for authenticating with the Photoprism API
|
|
|
|
//
|
|
|
|
// TODO @kris-nova obfuscate the data, and make immutable and unexported fields
|
2021-01-31 09:08:52 +01:00
|
|
|
type ClientAuthenticator interface {
|
|
|
|
getKey() string
|
|
|
|
getSecret() string
|
2021-02-04 06:01:21 +01:00
|
|
|
JSON() ([]byte, error)
|
2021-01-31 09:08:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// -- [ ClientAuthLogin ] --
|
|
|
|
|
|
|
|
type ClientAuthLogin struct {
|
2021-02-04 06:01:21 +01:00
|
|
|
// Request
|
|
|
|
Username string `json:"username"`
|
|
|
|
Password string `json:"password"`
|
|
|
|
|
|
|
|
// Response
|
2021-01-31 09:08:52 +01:00
|
|
|
}
|
|
|
|
|
2021-02-04 06:01:21 +01:00
|
|
|
// NewClientAuthLogin is used to build a new login struct
|
2021-01-31 09:08:52 +01:00
|
|
|
func NewClientAuthLogin(user, pass string) ClientAuthenticator {
|
|
|
|
return &ClientAuthLogin{
|
2021-02-04 06:01:21 +01:00
|
|
|
Username: user,
|
|
|
|
Password: pass,
|
2021-01-31 09:08:52 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *ClientAuthLogin) getKey() string {
|
2021-02-04 06:01:21 +01:00
|
|
|
return c.Username
|
2021-01-31 09:08:52 +01:00
|
|
|
}
|
|
|
|
func (c *ClientAuthLogin) getSecret() string {
|
2021-02-04 06:01:21 +01:00
|
|
|
return c.Password
|
2021-01-31 07:41:23 +01:00
|
|
|
}
|
|
|
|
|
2021-02-04 06:01:21 +01:00
|
|
|
// JSON is used to marshal the fields to JSON
|
|
|
|
func (c *ClientAuthLogin) JSON() ([]byte, error) {
|
|
|
|
return json.Marshal(c)
|
2021-01-31 09:08:52 +01:00
|
|
|
}
|
|
|
|
|
2021-02-04 06:01:21 +01:00
|
|
|
// V1 is used to access the V1 version of the Photoprism API
|
|
|
|
func (c *Client) V1() *api.V1Client {
|
|
|
|
return c.v1client
|
2021-01-31 09:08:52 +01:00
|
|
|
}
|
|
|
|
|
2021-02-04 06:01:21 +01:00
|
|
|
// Login is used to attempt to authenticate with the Photoprism API
|
2021-02-09 20:17:06 +01:00
|
|
|
func (c *Client) Auth(auth ClientAuthenticator) error {
|
|
|
|
c.authenticator = auth
|
2021-02-04 06:01:21 +01:00
|
|
|
// @kris-nova We are returning V1 by default
|
|
|
|
return c.LoginV1()
|
2021-01-31 09:08:52 +01:00
|
|
|
}
|
|
|
|
|
2021-02-04 06:01:21 +01:00
|
|
|
// POST /api/v1/session
|
|
|
|
//
|
|
|
|
// Data: {username: "admin", password: "missy"}
|
|
|
|
func (c *Client) LoginV1() error {
|
|
|
|
body, err := c.authenticator.JSON()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("JSON marshal error: %v", err)
|
|
|
|
}
|
|
|
|
buffer := bytes.NewBuffer(body)
|
2021-02-09 20:17:06 +01:00
|
|
|
resp, err := http.Post(c.Endpoint("api/v1/session"), c.contentType, buffer)
|
2021-02-04 06:01:21 +01:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("authentication error: %v", err)
|
|
|
|
}
|
2021-02-09 20:17:06 +01:00
|
|
|
if resp.StatusCode != 200 {
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to parse body for [%d] response: %v", resp.StatusCode, err)
|
|
|
|
}
|
|
|
|
return fmt.Errorf("login error [%d] %s", resp.StatusCode, body)
|
|
|
|
}
|
|
|
|
token := resp.Header.Get(DefaultTokenKey)
|
|
|
|
if token == "" {
|
|
|
|
return fmt.Errorf("missing auth token from successful login")
|
|
|
|
}
|
|
|
|
c.v1client = api.New(c.connectionString, token)
|
2021-02-04 06:01:21 +01:00
|
|
|
return nil
|
2021-01-31 07:41:23 +01:00
|
|
|
}
|
2021-01-31 09:08:52 +01:00
|
|
|
|
2021-02-04 06:01:21 +01:00
|
|
|
// Endpoint is used to calculate a FQN for a given API endpoint
|
|
|
|
// based on the API version and Host/Port
|
|
|
|
func (c *Client) Endpoint(str string) string {
|
|
|
|
if strings.HasPrefix("/", str) {
|
|
|
|
return fmt.Sprintf("%s%s", c.connectionString, str)
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("%s/%s", c.connectionString, str)
|
2021-01-31 09:08:52 +01:00
|
|
|
}
|
2021-02-04 06:01:21 +01:00
|
|
|
|
|
|
|
// ----------------------------------------------------------------------------------------
|
|
|
|
// Dump from Chrome/Network
|
|
|
|
|
|
|
|
// [REQUEST]
|
|
|
|
//Request URL: http://localhost:8080/api/v1/session
|
|
|
|
//Request Method: POST
|
|
|
|
//Status Code: 200 OK
|
|
|
|
//Remote Address: 127.0.0.1:8080
|
|
|
|
//Referrer Policy: strict-origin-when-cross-origin
|
|
|
|
|
|
|
|
// [RESPONSE HEADERS]
|
|
|
|
//Content-Type: application/json; charset=utf-8
|
|
|
|
//Date: Thu, 04 Feb 2021 03:27:03 GMT
|
|
|
|
//Transfer-Encoding: chunked
|
|
|
|
//X-Session-Id: d92837cb1c41e37b9993d25e282efb3b337b6ae609a687d9
|
|
|
|
|
|
|
|
// [REQUEST HEADERS]
|
|
|
|
//Accept: application/json, text/plain, */*
|
|
|
|
//Accept-Encoding: gzip, deflate, br
|
|
|
|
//Accept-Language: en-US,en;q=0.9
|
|
|
|
//Connection: keep-alive
|
|
|
|
//Content-Length: 39
|
|
|
|
//Content-Type: application/json;charset=UTF-8
|
|
|
|
//Host: localhost:8080
|
|
|
|
//Origin: http://localhost:8080
|
|
|
|
//Referer: http://localhost:8080/login
|
|
|
|
//Sec-Fetch-Dest: empty
|
|
|
|
//Sec-Fetch-Mode: cors
|
|
|
|
//Sec-Fetch-Site: same-origin
|
|
|
|
//User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36
|
|
|
|
//X-Client-Hash: 2607a5a5
|
|
|
|
//X-Client-Version: 210121-07e559df-Linux-x86_64
|
|
|
|
|
|
|
|
// [POST DATA]
|
|
|
|
//{username: "admin", password: "missy"}
|
|
|
|
//password: "missy"
|
|
|
|
//username: "admin"
|