405 lines
15 KiB
Markdown
405 lines
15 KiB
Markdown
# go-nc-exapp guide
|
|
|
|
How to use the library. Symbol signatures and doc comments are in [API](api.md). Words for the domain are in [CONTEXT.md](../CONTEXT.md).
|
|
|
|
## Credentials
|
|
|
|
### Purpose
|
|
|
|
`Credentials` is the set an ExApp needs to call Nextcloud as one user: the instance URL, the ExApp identity, the AppAPI secret, and the user id.
|
|
|
|
### When to use it
|
|
|
|
Fill one value at the edge of the service, from the environment HaRP injects, and pass it into preferences, notifications, groups, the Access Gate, and go-nc-files.
|
|
|
|
### Call sequence
|
|
|
|
1. Set `BaseURL`, `AppID`, `AppVersion`, `AAVersion`, `AppSecret`, and `UserID`.
|
|
2. Pass the value to `AuthHeaders`, `WithUser`, or a constructor such as `NewAppAPIPreferences`.
|
|
|
|
`BaseURL` may have a trailing slash. Callers in this package remove it when they build a URL.
|
|
|
|
### Errors
|
|
|
|
Constructing a `Credentials` value does not return an error. Empty fields are sent as empty headers. The Nextcloud call then fails.
|
|
|
|
### Example
|
|
|
|
```go
|
|
cred := gonexapp.Credentials{
|
|
BaseURL: "https://nextcloud.example",
|
|
AppID: "myexapp",
|
|
AppVersion: "0.1.0",
|
|
AAVersion: "1.0.0",
|
|
AppSecret: os.Getenv("APP_SECRET"),
|
|
UserID: "alice",
|
|
}
|
|
```
|
|
|
|
## AuthHeaders
|
|
|
|
### Purpose
|
|
|
|
`AuthHeaders` builds the AppAPI headers for a request from the ExApp to Nextcloud.
|
|
|
|
### When to use it
|
|
|
|
Use it for any Nextcloud HTTP call that is not going through `OCSClient`. `OCSClient` calls it for you. go-nc-files uses it on WebDAV.
|
|
|
|
### Call sequence
|
|
|
|
1. Fill `Credentials`.
|
|
2. Call `AuthHeaders`.
|
|
3. Copy the header onto the outbound request.
|
|
|
|
The set is `AA-VERSION`, `EX-APP-ID`, `EX-APP-VERSION`, `AUTHORIZATION-APP-API`, and `OCS-APIRequest`. `AUTHORIZATION-APP-API` is base64 of `UserID`, a colon, and `AppSecret`.
|
|
|
|
### Errors
|
|
|
|
`AuthHeaders` does not return an error.
|
|
|
|
### Example
|
|
|
|
```go
|
|
req.Header = cred.AuthHeaders()
|
|
```
|
|
|
|
## WithUser
|
|
|
|
### Purpose
|
|
|
|
`WithUser` returns a copy of the credentials whose `UserID` is the given user. The original value is unchanged.
|
|
|
|
### When to use it
|
|
|
|
Use it when one OCS call must run as a different user than the one on the service's credentials. `SendTo` and `UserGroups` do this themselves. Directory calls that must run as an admin use `WithUser` at the call site. This library does not pick an admin user.
|
|
|
|
### Call sequence
|
|
|
|
1. Start from the service credentials.
|
|
2. Call `WithUser` with the target user id.
|
|
3. Pass the copy to the client that should act as that user.
|
|
|
|
### Errors
|
|
|
|
`WithUser` does not return an error. An empty user id is stored as empty and fails later, when a call requires a recipient.
|
|
|
|
### Example
|
|
|
|
```go
|
|
asAdmin := cred.WithUser(adminID)
|
|
groups := gonexapp.NewGroups(asAdmin)
|
|
```
|
|
|
|
## UserFromRequest
|
|
|
|
### Purpose
|
|
|
|
`UserFromRequest` reads the requesting user from `AUTHORIZATION-APP-API` on a request HaRP proxied into the ExApp.
|
|
|
|
### When to use it
|
|
|
|
Use it on inbound ExApp routes when the handler needs the user id and does not already have it from the Access Gate. The Access Gate calls it before checking groups.
|
|
|
|
### Call sequence
|
|
|
|
1. Take the `*http.Request` the ExApp received.
|
|
2. Call `UserFromRequest`.
|
|
3. Use the returned user id as `Credentials.UserID` for outbound calls on behalf of that user.
|
|
|
|
### Errors
|
|
|
|
The function returns an error when the header is missing, is not base64, or has no user id before the colon.
|
|
|
|
### Example
|
|
|
|
```go
|
|
userID, err := gonexapp.UserFromRequest(r)
|
|
if err != nil {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
cred.UserID = userID
|
|
```
|
|
|
|
## OCSClient
|
|
|
|
### Purpose
|
|
|
|
`OCSClient` performs an AppAPI-authenticated OCS request and requires a JSON body. Every URL gets `format=json`.
|
|
|
|
### When to use it
|
|
|
|
Use it for OCS routes that the typed helpers do not cover. Preferences, notifications, and groups are built on it. A nil `Client` uses `http.DefaultClient`.
|
|
|
|
### Call sequence
|
|
|
|
1. Set `OCSClient.Cred`.
|
|
2. Call `URL` when the caller only needs the address, or `Call` to perform the request.
|
|
3. `Call` takes an HTTP method, a path under `/ocs/v2.php/`, and an optional JSON body.
|
|
4. On success, decode the returned bytes. The client checks that the bytes are JSON and does not decode a particular OCS shape.
|
|
|
|
`URL` joins `BaseURL`, `/ocs/v2.php/`, the path, and `?format=json`.
|
|
|
|
### Errors
|
|
|
|
`Call` returns an error when the request cannot be built, the transport fails, the status is outside 2xx, or the body is not JSON. A non-2xx error includes the method, path, and HTTP status.
|
|
|
|
### Example
|
|
|
|
```go
|
|
ocs := gonexapp.OCSClient{Cred: cred}
|
|
raw, err := ocs.Call(http.MethodGet, "cloud/capabilities", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_ = raw
|
|
```
|
|
|
|
## AppAPIPreferences
|
|
|
|
### Purpose
|
|
|
|
`AppAPIPreferences` reads and writes one string ExApp preference for the requesting user. The caller chooses the app id and the key. The library does not reserve product key names.
|
|
|
|
### When to use it
|
|
|
|
Use it for a per-user string such as a Saved Default path. Pair it with a go-nc-files `DefaultPathStore` in the ExApp if the product stores that path in preferences.
|
|
|
|
### Call sequence
|
|
|
|
1. Call `NewAppAPIPreferences` with credentials, the app id, and the key.
|
|
2. `Get` loads the value. A missing key returns an empty string.
|
|
3. `Set` stores a new string. The value is stored as non-sensitive.
|
|
|
|
Declare nothing extra for the preference itself. The AppAPI preference routes are fixed.
|
|
|
|
### Errors
|
|
|
|
`Get` and `Set` return the error from the OCS call. `Get` also returns an error when the body cannot be decoded. A missing key is an empty string and a nil error.
|
|
|
|
### Example
|
|
|
|
```go
|
|
prefs := gonexapp.NewAppAPIPreferences(cred, "myexapp", "savedDefault")
|
|
value, err := prefs.Get()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := prefs.Set("Zones"); err != nil {
|
|
return err
|
|
}
|
|
_ = value
|
|
```
|
|
|
|
## AppAPINotifications
|
|
|
|
### Purpose
|
|
|
|
`AppAPINotifications` creates one Nextcloud bell notification for one recipient. `Notification` carries a subject and, optionally, a message, a link, and rich-object parameter maps.
|
|
|
|
### When to use it
|
|
|
|
Use `Send` for the user on `Credentials`. Use `SendTo` for a different user. The library does not fan out to a group or to all admins.
|
|
|
|
### Call sequence
|
|
|
|
1. Call `NewAppAPINotifications` with credentials.
|
|
2. Fill a `Notification`. `Subject` is required. `Message`, `Link`, `SubjectParams`, and `MessageParams` are optional.
|
|
3. Call `Send` or `SendTo`.
|
|
|
|
`SendTo` authenticates the OCS call as that user via `WithUser`. AppAPI's notification route accepts this shape and does not accept actions or a custom icon.
|
|
|
|
### Errors
|
|
|
|
`Send` returns an error when `Cred.UserID` is empty. Both methods return an error when the subject is empty, the recipient user id is empty, or the OCS call fails.
|
|
|
|
### Example
|
|
|
|
```go
|
|
err := gonexapp.NewAppAPINotifications(cred).Send(gonexapp.Notification{
|
|
Subject: "Job finished",
|
|
Message: "The zone was updated.",
|
|
})
|
|
```
|
|
|
|
## Groups
|
|
|
|
### Purpose
|
|
|
|
`Groups` reads Users and Groups from the Provisioning API: the groups of one user, the members of one group, and the instance group list. There is no search and no paging.
|
|
|
|
### When to use it
|
|
|
|
Use `UserGroups` for the Access Gate's membership check, or whenever the product needs one user's groups. Use `GroupMembers` and `ListGroups` for directory listings. Those two call OCS as `Cred`'s user, who must be an admin or a subadmin.
|
|
|
|
### Call sequence
|
|
|
|
1. Call `NewGroups` with credentials.
|
|
2. `UserGroups(userID)` lists that user's group ids and authenticates as `userID`.
|
|
3. `GroupMembers(groupID)` lists user ids in the group, as the credentials user.
|
|
4. `ListGroups` lists instance group ids, as the credentials user.
|
|
|
|
### Errors
|
|
|
|
Each method returns an error when the OCS call fails or the body cannot be decoded. A forbidden directory call is the OCS error from `Call` (HTTP status in the message). This package does not wrap those as a sentinel.
|
|
|
|
### Example
|
|
|
|
```go
|
|
members, err := gonexapp.NewGroups(cred).GroupMembers("ops")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_ = members
|
|
```
|
|
|
|
## Access Gate
|
|
|
|
### Purpose
|
|
|
|
The Access Gate allows a request only when the requesting user is in one of the Required Groups. An empty group list turns the gate off.
|
|
|
|
### When to use it
|
|
|
|
Wrap the ExApp's HTTP handler when the product restricts who may open it. Leave `Groups` empty to allow everyone. Lifecycle routes stay reachable either way.
|
|
|
|
### Call sequence
|
|
|
|
1. Read the deploy environment. `EnvRequiredGroups` is `REQUIRED_GROUPS`. `EnvRequiredGroupsCacheSeconds` is `REQUIRED_GROUPS_CACHE_SECONDS`.
|
|
2. `ResolveRequiredGroups` uses the code default when the variable is unset, and replaces it when the variable is set, including set to empty.
|
|
3. `ParseCacheSeconds` turns the cache variable into a duration. Unset or invalid uses `DefaultCacheSeconds` (60). `"0"` disables the cache.
|
|
4. Put the credentials, the group list, and the cache TTL on an `AccessGate`.
|
|
5. `Wrap` the handler. Or call `Check` and branch on `CheckResult`.
|
|
|
|
`Wrap` calls `next` on `CheckAllowed`. `CheckUnauthorized` writes 401. `CheckUnavailable` writes 503. `CheckDenied` writes English HTML with status 200 and `frame-ancestors 'self'` when the request accepts `text/html`, and 403 otherwise. The 200 status and the CSP keep AppAPI from blanking the iframe.
|
|
|
|
Skipped paths are `/heartbeat`, `/enabled`, `/init`, anything under `/js/`, and `ExtraSkipPaths`. Top-menu scripts live under `/js/` so a non-member can still load them.
|
|
|
|
Positive membership is cached for `CacheTTL`. A denial is not cached. `CacheTTL` of zero disables the cache.
|
|
|
|
### Errors
|
|
|
|
`Check` does not return an error. It returns `CheckUnauthorized` when the requesting user cannot be read, and `CheckUnavailable` when the group lookup fails. `ParseRequiredGroups` and `ResolveRequiredGroups` do not return errors. They drop empty comma-separated fields.
|
|
|
|
### Example
|
|
|
|
```go
|
|
groupsEnv, groupsSet := os.LookupEnv(gonexapp.EnvRequiredGroups)
|
|
groups := gonexapp.ResolveRequiredGroups(groupsEnv, groupsSet, nil)
|
|
ttl := gonexapp.ParseCacheSeconds(os.Getenv(gonexapp.EnvRequiredGroupsCacheSeconds), gonexapp.DefaultCacheSeconds)
|
|
handler = gonexapp.AccessGate{Cred: cred, Groups: groups, CacheTTL: ttl}.Wrap(handler)
|
|
```
|
|
|
|
## Top Menu visibility
|
|
|
|
### Purpose
|
|
|
|
`TopMenuAdminRequired` turns the deploy environment into the `"0"` or `"1"` string AppAPI's top-menu OCS field `adminRequired` expects.
|
|
|
|
### When to use it
|
|
|
|
Use it when the ExApp registers its top-menu entry at enable time. The library does not register the menu. `"1"` means admins only. `"0"` means every user.
|
|
|
|
### Call sequence
|
|
|
|
1. Declare `TOP_MENU_ADMIN_REQUIRED` (`EnvTopMenuAdminRequired`) in `info.xml` under environment variables.
|
|
2. At enable time, call `TopMenuAdminRequired` with the env value and `DefaultTopMenuAdminRequired` (`true`).
|
|
3. Send the returned `"0"` or `"1"` in the top-menu registration request.
|
|
|
|
Only `"0"` and `"1"` are accepted. Any other value, including empty, uses the default. Changing the deploy env does not update an entry AppAPI already registered. Set the new value, recreate or restart the container so the env is present, then disable and enable the ExApp (or update it) so `PUT /enabled?enabled=1` runs again. Route `access_level` in `info.xml` is separate and changes only when AppAPI re-reads `info.xml`.
|
|
|
|
### Errors
|
|
|
|
`TopMenuAdminRequired` does not return an error. An invalid value falls back to the default.
|
|
|
|
### Example
|
|
|
|
```go
|
|
adminRequired := gonexapp.TopMenuAdminRequired(
|
|
os.Getenv(gonexapp.EnvTopMenuAdminRequired),
|
|
gonexapp.DefaultTopMenuAdminRequired,
|
|
)
|
|
```
|
|
|
|
## App navigation
|
|
|
|
### Purpose
|
|
|
|
App navigation is an optional Files-style shell. The ExApp supplies the tree and the page for each item. Mounting `Handler` replaces the ExApp's own page. Not mounting it leaves that page alone.
|
|
|
|
### When to use it
|
|
|
|
Use it when the ExApp wants Nextcloud's navigation column, theme background, and a selected item in the query string. Lifecycle routes, HaRP startup, and top-menu registration stay in the ExApp.
|
|
|
|
### Call sequence
|
|
|
|
1. Fill `AppNavigation`. `Items`, `Page`, and `DefaultID` are required. `Header` and `MissingMessage` are optional.
|
|
2. `Items` returns the tree for this request. An `Item` has an id, a label, optional children, and an optional `Icon`.
|
|
3. Set `Icon` to a same-origin image URL, or to a field of `NextcloudIcons` (core SVGs Nextcloud already serves, such as `NextcloudIcons.Folder`).
|
|
4. `Page` returns the HTML body for the selected id and whether that id is known.
|
|
5. `SelectKey` defaults to `item`. Other query parameters, including a Visit folder, stay on the address.
|
|
6. Mount `Handler` on the ExApp's page route.
|
|
|
|
A missing item renders `DefaultID`, shows `MissingMessage`, and publishes the corrected query on `data-address`. Every parent starts expanded. At 1024px and below, the tree stays hidden until the user opens it. Choosing an item, pressing Escape, or clicking outside closes it.
|
|
|
|
The zero value of `DisableTheme` loads the Nextcloud theme stylesheets, paints `--image-background`, and copies the surrounding page's `data-theme-*` markers. Set `DisableTheme` to skip the stylesheets. The shell includes the Dialog. The response sets `frame-ancestors 'self'` so AppAPI can show the iframe.
|
|
|
|
### Errors
|
|
|
|
`Handler` does not return an error to the caller. It writes HTML with status 200. A nil `Items` or `Page` yields an empty tree or an empty page.
|
|
|
|
### Example
|
|
|
|
```go
|
|
nav := gonexapp.AppNavigation{
|
|
DefaultID: "home",
|
|
Items: func(*http.Request) []gonexapp.Item {
|
|
return []gonexapp.Item{{
|
|
ID: "home", Label: "Home", Icon: gonexapp.NextcloudIcons.Home,
|
|
}}
|
|
},
|
|
Page: func(_ *http.Request, id string) (string, bool) {
|
|
if id != "home" {
|
|
return "", false
|
|
}
|
|
return "<p>Home</p>", true
|
|
},
|
|
}
|
|
http.Handle("/page", nav.Handler())
|
|
```
|
|
|
|
## Dialog
|
|
|
|
### Purpose
|
|
|
|
The Dialog is a modal for a message, a confirm, or a prompt. The page calls it from JavaScript and waits for the user's choice.
|
|
|
|
### When to use it
|
|
|
|
App navigation already includes the dialog. Insert `DialogHTML` on a page the ExApp renders itself. The buttons say OK and Cancel. The agreeing button's word may be replaced. The choice stays in the page.
|
|
|
|
### Call sequence
|
|
|
|
1. Insert `DialogHTML()` into the page HTML once.
|
|
2. From the page script, call one of `exappDialog.message`, `exappDialog.confirm`, or `exappDialog.prompt`, and await the promise.
|
|
|
|
`message` takes `heading`, `text`, and `severity` (`info`, `warning`, or `error`). It shows no Cancel button. `confirm` takes `heading`, `text`, optional `agree`, and optional `destructive`. It resolves `true` or `false`. `prompt` takes `heading`, `text`, optional `value`, and optional `agree`. It resolves the trimmed string, or `null` when the user cancels. The agreeing button stays disabled until the field is non-empty. Calls are queued so only one dialog is open.
|
|
|
|
### Errors
|
|
|
|
`DialogHTML` does not return an error. It returns HTML that is safe to drop into a `template.HTML` context because the function's result type is already `template.HTML`. The page script is responsible for handling a dismissed dialog (`false`, `null`, or a resolved message).
|
|
|
|
### Example
|
|
|
|
```go
|
|
fmt.Fprint(w, gonexapp.DialogHTML())
|
|
```
|
|
|
|
```html
|
|
<script>
|
|
const ok = await exappDialog.confirm({heading: "Delete", text: "Delete this file?", agree: "Delete", destructive: true});
|
|
const name = await exappDialog.prompt({heading: "Name", text: "Folder name", value: "Zones"});
|
|
</script>
|
|
```
|