feat: support Remember Me for prolonging session validity on user/pass authentication (#52)

This commit is contained in:
Mateusz Filipowicz
2025-03-16 11:48:44 +01:00
committed by GitHub
parent 473cd3f1ed
commit 873818ddac
4 changed files with 182 additions and 10 deletions

View File

@@ -29,6 +29,7 @@ c, err := unifi.NewClient(&unifi.ClientConfig{
BaseURL: "https://unifi.localdomain", BaseURL: "https://unifi.localdomain",
Username: "your-username", Username: "your-username",
Password: "your-password", Password: "your-password",
RememberMe: true, // Optional: prolong the session validity. Might be needed for long-running applications.
}) })
if err != nil { if err != nil {
log.Fatalf("Error creating client: %v", err) log.Fatalf("Error creating client: %v", err)

149
docs/file_uploads.md Normal file
View File

@@ -0,0 +1,149 @@
# File Uploads in go-unifi
This document describes how to use the file upload functionality in the go-unifi client.
## Overview
The go-unifi client provides two methods for uploading files to the UniFi controller:
1. `UploadFile` - Upload a file from a file path on disk
2. `UploadFileFromReader` - Upload a file from an `io.Reader` (e.g., from memory, network stream, etc.)
Both methods use the `multipart/form-data` format for file uploads, which is required by the UniFi controller.
## Examples
### Uploading a file from disk
```go
package main
import (
"context"
"log"
"github.com/filipowm/go-unifi/unifi"
)
func main() {
// Create a client
client, err := unifi.NewClient(&unifi.ClientConfig{
URL: "https://your-unifi-controller:8443",
User: "your-username",
Password: "your-password",
})
if err != nil {
log.Fatalf("Error creating client: %v", err)
}
// Prepare any additional form fields if needed
formFields := map[string]string{
"description": "My uploaded file",
}
// Upload the file to the controller
var response map[string]interface{} // Adjust this type based on the expected response
err = client.UploadFile(
context.Background(),
"/api/s/default/upload", // The API endpoint to upload to
"/path/to/your/file.txt", // Path to the file on disk
"file", // Form field name for the file
formFields, // Additional form fields
&response, // Response structure to capture the result
)
if err != nil {
log.Fatalf("Error uploading file: %v", err)
}
log.Printf("Upload successful: %v", response)
}
```
### Uploading a file from memory
```go
package main
import (
"bytes"
"context"
"log"
"github.com/paultyng/go-unifi/unifi"
)
func main() {
// Create a client
client, err := unifi.NewClient(&unifi.ClientConfig{
URL: "https://your-unifi-controller:8443",
User: "your-username",
Password: "your-password",
})
if err != nil {
log.Fatalf("Error creating client: %v", err)
}
// Create file content in memory
fileContent := []byte("This is some test content to upload")
reader := bytes.NewReader(fileContent)
// Upload the file from the reader
var response map[string]interface{} // Adjust this type based on the expected response
err = client.UploadFileFromReader(
context.Background(),
"/api/s/default/upload", // The API endpoint to upload to
reader, // Reader with the file content
"myfile.txt", // Filename to use in the upload
"file", // Form field name for the file
nil, // No additional form fields
&response, // Response structure to capture the result
)
if err != nil {
log.Fatalf("Error uploading file: %v", err)
}
log.Printf("Upload successful: %v", response)
}
```
## API Reference
### UploadFile
```go
func (c *client) UploadFile(ctx context.Context, apiPath, filePath, fieldName string, formFields map[string]string, respBody interface{}) error
```
Uploads a file to the UniFi controller from a file path.
Parameters:
- `ctx`: The context for the request
- `apiPath`: The API endpoint path to upload the file to
- `filePath`: Path to the file on disk
- `fieldName`: Form field name for the file (defaults to "file" if empty)
- `formFields`: Additional form fields to include in the upload (can be nil)
- `respBody`: Structure to decode the response into (can be nil)
### UploadFileFromReader
```go
func (c *client) UploadFileFromReader(ctx context.Context, apiPath string, reader io.Reader, filename, fieldName string, formFields map[string]string, respBody interface{}) error
```
Uploads a file to the UniFi controller from an io.Reader.
Parameters:
- `ctx`: The context for the request
- `apiPath`: The API endpoint path to upload the file to
- `reader`: Reader with the file content
- `filename`: Name of the file to use in the upload
- `fieldName`: Form field name for the file (defaults to "file" if empty)
- `formFields`: Additional form fields to include in the upload (can be nil)
- `respBody`: Structure to decode the response into (can be nil)
## Notes
- These methods use `POST` requests for file uploads
- The UniFi controller typically expects files to be uploaded with the field name "file", but this can be changed as needed
- The content type for the request is automatically set to "multipart/form-data" with the correct boundary
- All existing client features like interceptors, error handling, and request validation are preserved

View File

@@ -65,6 +65,20 @@ if err != nil {
} }
``` ```
You can also configure `Remember Me` option, which will prolong the session validity. Might be required for long-running applications, that require authentication only once.
```go
c, err := unifi.NewClient(&unifi.ClientConfig{
BaseURL: "https://unifi.localdomain",
Username: "your-username",
Password: "your-password",
RememberMe: true,
})
if err != nil {
log.Fatalf("Failed to create client: %v", err)
}
```
### Bare Client Initialization ### Bare Client Initialization
You can also use bare client, which creates a `unifi.Client` without initialization like logging in and getting system information. This can be useful in specific scenarios, when doing such initialization might be an uneeded overhead. To create it you can use the `NewBareClient` function provided in the SDK (see `unifi/client.go`). You can also use bare client, which creates a `unifi.Client` without initialization like logging in and getting system information. This can be useful in specific scenarios, when doing such initialization might be an uneeded overhead. To create it you can use the `NewBareClient` function provided in the SDK (see `unifi/client.go`).

View File

@@ -46,7 +46,8 @@ Fields:
URL: The base URL of the UniFi controller. Must be a valid URL and should not include the `/api` suffix. URL: The base URL of the UniFi controller. Must be a valid URL and should not include the `/api` suffix.
APIKey: An API key used for authentication. Provide this if user/password credentials are not used. APIKey: An API key used for authentication. Provide this if user/password credentials are not used.
User: The username for user/password authentication. Must be provided with Password if APIKey is not used. User: The username for user/password authentication. Must be provided with Password if APIKey is not used.
Password: The password for user/password authentication. Must be provided with User if APIKey is not used. Password: The password for user/password authentication. Must be provided with User if APIKey is not used.
RememberMe: If true, the session is remembered for future requests. Useful for long-running processes. Default: false. Only used for user/password authentication.
Timeout: The maximum duration to wait for responses; default is no timeout. Timeout: The maximum duration to wait for responses; default is no timeout.
VerifySSL: When false, disables SSL certificate verification. VerifySSL: When false, disables SSL certificate verification.
Interceptors: A slice of ClientInterceptor implementations that can modify requests and responses. Interceptors: A slice of ClientInterceptor implementations that can modify requests and responses.
@@ -62,6 +63,7 @@ type ClientConfig struct {
APIKey string `validate:"required_without_all=User Password"` APIKey string `validate:"required_without_all=User Password"`
User string `validate:"excluded_with=APIKey,required_with=Password"` User string `validate:"excluded_with=APIKey,required_with=Password"`
Password string `validate:"excluded_with=APIKey,required_with=User"` Password string `validate:"excluded_with=APIKey,required_with=User"`
RememberMe bool `validate:"excluded_with=APIKey"`
Timeout time.Duration // How long to wait for replies, default: forever. Timeout time.Duration // How long to wait for replies, default: forever.
VerifySSL bool VerifySSL bool
Interceptors []ClientInterceptor Interceptors []ClientInterceptor
@@ -85,6 +87,7 @@ type Credentials interface {
GetUser() string GetUser() string
// GetPass returns the password for authentication; returns an empty string if not applicable. // GetPass returns the password for authentication; returns an empty string if not applicable.
GetPass() string GetPass() string
IsRememberMe() bool
} }
// APIKeyCredentials holds API key authentication details. // APIKeyCredentials holds API key authentication details.
@@ -92,21 +95,24 @@ type APIKeyCredentials struct {
APIKey string APIKey string
} }
func (a APIKeyCredentials) IsAPIKey() bool { return true } func (a APIKeyCredentials) IsAPIKey() bool { return true }
func (a APIKeyCredentials) GetAPIKey() string { return a.APIKey } func (a APIKeyCredentials) GetAPIKey() string { return a.APIKey }
func (a APIKeyCredentials) GetUser() string { return "" } func (a APIKeyCredentials) GetUser() string { return "" }
func (a APIKeyCredentials) GetPass() string { return "" } func (a APIKeyCredentials) GetPass() string { return "" }
func (a APIKeyCredentials) IsRememberMe() bool { return false }
// UserPassCredentials holds user/password authentication. // UserPassCredentials holds user/password authentication.
type UserPassCredentials struct { type UserPassCredentials struct {
User string User string
Password string Password string
Remember bool
} }
func (u UserPassCredentials) IsAPIKey() bool { return false } func (u UserPassCredentials) IsAPIKey() bool { return false }
func (u UserPassCredentials) GetAPIKey() string { return "" } func (u UserPassCredentials) GetAPIKey() string { return "" }
func (u UserPassCredentials) GetUser() string { return u.User } func (u UserPassCredentials) GetUser() string { return u.User }
func (u UserPassCredentials) GetPass() string { return u.Password } func (u UserPassCredentials) GetPass() string { return u.Password }
func (u UserPassCredentials) IsRememberMe() bool { return u.Remember }
// client represents a UniFi client. // client represents a UniFi client.
type client struct { type client struct {
@@ -219,7 +225,7 @@ func newClientFromConfig(config *ClientConfig, v *validator) (*client, error) {
interceptors = append(interceptors, &APIKeyAuthInterceptor{apiKey: config.APIKey}) interceptors = append(interceptors, &APIKeyAuthInterceptor{apiKey: config.APIKey})
} else { } else {
log.Debug("Using user/pass authentication") log.Debug("Using user/pass authentication")
credentials = UserPassCredentials{User: config.User, Password: config.Password} credentials = UserPassCredentials{User: config.User, Password: config.Password, Remember: config.RememberMe}
interceptors = append(interceptors, &CSRFInterceptor{}) interceptors = append(interceptors, &CSRFInterceptor{})
} }
if len(config.UserAgent) == 0 { if len(config.UserAgent) == 0 {
@@ -322,9 +328,11 @@ func (c *client) Login() error {
err := c.Post(ctx, c.apiPaths.LoginPath, &struct { err := c.Post(ctx, c.apiPaths.LoginPath, &struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
Remember bool `json:"remember"`
}{ }{
Username: c.credentials.GetUser(), Username: c.credentials.GetUser(),
Password: c.credentials.GetPass(), Password: c.credentials.GetPass(),
Remember: c.credentials.IsRememberMe(),
}, nil) }, nil)
if err != nil { if err != nil {
return err return err