Rename public group URLs to halls
This commit is contained in:
@@ -15,7 +15,7 @@ echo '{"users": {"vimes": {"password":"sybil", "permissions":"op"}}}' > groups/n
|
|||||||
./skald &
|
./skald &
|
||||||
```
|
```
|
||||||
|
|
||||||
Point your browser at <https://localhost:8443/group/night-watch/>, ignore
|
Point your browser at <https://localhost:8443/hall/night-watch/>, ignore
|
||||||
the unknown certificate warning, and log in with username *vimes* and
|
the unknown certificate warning, and log in with username *vimes* and
|
||||||
password *sybil*.
|
password *sybil*.
|
||||||
|
|
||||||
|
|||||||
+21
-21
@@ -1,22 +1,22 @@
|
|||||||
# Skald's administrative API
|
# Skald's administrative API
|
||||||
|
|
||||||
Skald provides an HTTP-based API that can be used to create groups and
|
Skald provides an HTTP-based API that can be used to create groups and
|
||||||
users. For example, in order to create a group, a client may do
|
users. For example, in order to create a hall, a client may do
|
||||||
|
|
||||||
PUT /skald-api/v0/.groups/groupname/
|
PUT /skald-api/v0/.halls/hallname/
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
If-None-Match: *
|
If-None-Match: *
|
||||||
|
|
||||||
The `If-None-Match` header avoids overwriting an existing group.
|
The `If-None-Match` header avoids overwriting an existing group.
|
||||||
|
|
||||||
In order to edit a group definition, a client first does
|
In order to edit a hall definition, a client first does
|
||||||
|
|
||||||
GET /skald-api/v0/.groups/groupname/
|
GET /skald-api/v0/.halls/hallname/
|
||||||
|
|
||||||
This yields the group definition and an entity tag (in the ETag header).
|
This yields the hall definition and an entity tag (in the ETag header).
|
||||||
The client then modifies the group defintion, and does
|
The client then modifies the group defintion, and does
|
||||||
|
|
||||||
PUT /skald-api/v0/.groups/groupname/
|
PUT /skald-api/v0/.halls/hallname/
|
||||||
If-Match: "abcd"
|
If-Match: "abcd"
|
||||||
|
|
||||||
where "abcd" is the entity tag returned by the GET request. If the group
|
where "abcd" is the entity tag returned by the GET request. If the group
|
||||||
@@ -41,23 +41,23 @@ allowed methods are HEAD and GET.
|
|||||||
|
|
||||||
### List of groups
|
### List of groups
|
||||||
|
|
||||||
/skald-api/v0/.groups/
|
/skald-api/v0/.halls/
|
||||||
|
|
||||||
Returns a list of groups, as a JSON array. The only allowed methods are
|
Returns a list of groups, as a JSON array. The only allowed methods are
|
||||||
HEAD and GET.
|
HEAD and GET.
|
||||||
|
|
||||||
### Group definition
|
### Group definition
|
||||||
|
|
||||||
/skald-api/v0/.groups/groupname
|
/skald-api/v0/.halls/hallname
|
||||||
|
|
||||||
Contains a "sanitised" group definition in JSON format, analogous to the
|
Contains a "sanitised" hall definition in JSON format, analogous to the
|
||||||
on-disk format but without any user definitions or cryptographic keys.
|
on-disk format but without any user definitions or cryptographic keys.
|
||||||
Allowed methods are HEAD, GET, PUT and DELETE. The only accepted
|
Allowed methods are HEAD, GET, PUT and DELETE. The only accepted
|
||||||
content-type is `application/json`.
|
content-type is `application/json`.
|
||||||
|
|
||||||
### Authentication keys
|
### Authentication keys
|
||||||
|
|
||||||
/skald-api/v0/.groups/groupname/.keys
|
/skald-api/v0/.halls/hallname/.keys
|
||||||
|
|
||||||
Contains the keys used for validation of stateless tokens, encoded as
|
Contains the keys used for validation of stateless tokens, encoded as
|
||||||
a JSON key set (RFC 7517). Allowed methods are PUT and DELETE. The only
|
a JSON key set (RFC 7517). Allowed methods are PUT and DELETE. The only
|
||||||
@@ -65,16 +65,16 @@ accepted content-type is `application/jwk-set+json`.
|
|||||||
|
|
||||||
### List of users
|
### List of users
|
||||||
|
|
||||||
/skald-api/v0/.groups/groupname/.users/
|
/skald-api/v0/.halls/hallname/.users/
|
||||||
|
|
||||||
Returns a list of users, as a JSON array. The only allowed methods are
|
Returns a list of users, as a JSON array. The only allowed methods are
|
||||||
HEAD and GET.
|
HEAD and GET.
|
||||||
|
|
||||||
### User definitions
|
### User definitions
|
||||||
|
|
||||||
/skald-api/v0/.groups/groupname/.users/username
|
/skald-api/v0/.halls/hallname/.users/username
|
||||||
/skald-api/v0/.groups/groupname/.empty-user
|
/skald-api/v0/.halls/hallname/.empty-user
|
||||||
/skald-api/v0/.groups/groupname/.wildcard-user
|
/skald-api/v0/.halls/hallname/.wildcard-user
|
||||||
|
|
||||||
Contains a "sanitised" user definition (without any passwords), a JSON
|
Contains a "sanitised" user definition (without any passwords), a JSON
|
||||||
object with a single field `permissions`. The entries `.empty-user` and
|
object with a single field `permissions`. The entries `.empty-user` and
|
||||||
@@ -84,9 +84,9 @@ only accepted content-type is `application/json`.
|
|||||||
|
|
||||||
### Passwords
|
### Passwords
|
||||||
|
|
||||||
/skald-api/v0/.groups/groupname/.users/username/.password
|
/skald-api/v0/.halls/hallname/.users/username/.password
|
||||||
/skald-api/v0/.groups/groupname/.empty-user/.password
|
/skald-api/v0/.halls/hallname/.empty-user/.password
|
||||||
/skald-api/v0/.groups/groupname/.wildcard-user/.password
|
/skald-api/v0/.halls/hallname/.wildcard-user/.password
|
||||||
|
|
||||||
Contains the password of a given user. The PUT method takes a full
|
Contains the password of a given user. The PUT method takes a full
|
||||||
password definition, identical to what can appear in the `"password"`
|
password definition, identical to what can appear in the `"password"`
|
||||||
@@ -97,7 +97,7 @@ POST.
|
|||||||
|
|
||||||
### Wildcard user
|
### Wildcard user
|
||||||
|
|
||||||
/skald-api/v0/.groups/groupname/.wildcard-user
|
/skald-api/v0/.halls/hallname/.wildcard-user
|
||||||
|
|
||||||
Contains a dictionary defining the wildcard user, in the same format as
|
Contains a dictionary defining the wildcard user, in the same format as
|
||||||
the dictionary defining an ordinary user. Allowed methods are HEAD, GET,
|
the dictionary defining an ordinary user. Allowed methods are HEAD, GET,
|
||||||
@@ -105,14 +105,14 @@ PUT and DELETE.
|
|||||||
|
|
||||||
### Wildcard user password
|
### Wildcard user password
|
||||||
|
|
||||||
/skald-api/v0/.groups/groupname/.wildcard-user/.password
|
/skald-api/v0/.halls/hallname/.wildcard-user/.password
|
||||||
|
|
||||||
This is analogous to the password of an ordinary user. Allowed methods
|
This is analogous to the password of an ordinary user. Allowed methods
|
||||||
are PUT, POST and DELETE.
|
are PUT, POST and DELETE.
|
||||||
|
|
||||||
### List of stateful tokens
|
### List of stateful tokens
|
||||||
|
|
||||||
/skald-api/v0/.groups/groupname/.users/username/.tokens/
|
/skald-api/v0/.halls/hallname/.users/username/.tokens/
|
||||||
|
|
||||||
GET returns the list of stateful tokens, as a JSON array. POST creates
|
GET returns the list of stateful tokens, as a JSON array. POST creates
|
||||||
a new token, and returns its name in the `Location` header. Allowed
|
a new token, and returns its name in the `Location` header. Allowed
|
||||||
@@ -120,7 +120,7 @@ methods are HEAD, GET and POST.
|
|||||||
|
|
||||||
### Stateful token
|
### Stateful token
|
||||||
|
|
||||||
/skald-api/v0/.groups/groupname/.users/username/.tokens/token
|
/skald-api/v0/.halls/hallname/.users/username/.tokens/token
|
||||||
|
|
||||||
The full contents of a single token, in JSON. The exact format may change
|
The full contents of a single token, in JSON. The exact format may change
|
||||||
between versions, so a client should first GET a token, update one or more
|
between versions, so a client should first GET a token, update one or more
|
||||||
|
|||||||
+10
-10
@@ -159,46 +159,46 @@ rsync config.json skald@skald.example.org:data/
|
|||||||
Create a group:
|
Create a group:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl create-group -group city-watch
|
skaldctl create-hall -hall city-watch
|
||||||
```
|
```
|
||||||
|
|
||||||
If you didn't install a TLS certificate above, you will need to run
|
If you didn't install a TLS certificate above, you will need to run
|
||||||
`skaldctl` with the flag `-insecure`:
|
`skaldctl` with the flag `-insecure`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl -insecure create-group -group city-watch
|
skaldctl -insecure create-hall -hall city-watch
|
||||||
```
|
```
|
||||||
|
|
||||||
Create an "op", a user with group moderation privileges:
|
Create an "op", a user with group moderation privileges:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl create-user -group city-watch -user vimes -permissions op
|
skaldctl create-user -hall city-watch -user vimes -permissions op
|
||||||
```
|
```
|
||||||
|
|
||||||
Set the new user's password:
|
Set the new user's password:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl set-password -group city-watch -user vimes
|
skaldctl set-password -hall city-watch -user vimes
|
||||||
```
|
```
|
||||||
|
|
||||||
You should now be able to test your Skald installation by pointing a web
|
You should now be able to test your Skald installation by pointing a web
|
||||||
browser at <https://skald.example.org:8443/group/city-watch/>.
|
browser at <https://skald.example.org:8443/hall/city-watch/>.
|
||||||
|
|
||||||
Create an ordinary user:
|
Create an ordinary user:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl create-user -group city-watch -user fred
|
skaldctl create-user -hall city-watch -user fred
|
||||||
skaldctl set-password -group city-watch -user fred
|
skaldctl set-password -hall city-watch -user fred
|
||||||
```
|
```
|
||||||
|
|
||||||
Check the results:
|
Check the results:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl list-groups
|
skaldctl list-halls
|
||||||
skaldctl list-users -l -group city-watch
|
skaldctl list-users -l -hall city-watch
|
||||||
```
|
```
|
||||||
|
|
||||||
Type `skaldctl -help`, `skaldctl create-group -help`, etc. for more
|
Type `skaldctl -help`, `skaldctl create-hall -help`, etc. for more
|
||||||
information.
|
information.
|
||||||
|
|
||||||
## Advanced configuration
|
## Advanced configuration
|
||||||
|
|||||||
+4
-4
@@ -30,7 +30,7 @@ server to client direction.
|
|||||||
|
|
||||||
The client needs to know the location of the group, the (user-visible) URL
|
The client needs to know the location of the group, the (user-visible) URL
|
||||||
at which the group is found. This may be obtained either by explicit
|
at which the group is found. This may be obtained either by explicit
|
||||||
configuration by the user, or by parsing the `/public-groups.json` file
|
configuration by the user, or by parsing the `/public-halls.json` file
|
||||||
which may contain an array of group statuses (see below).
|
which may contain an array of group statuses (see below).
|
||||||
|
|
||||||
A client then performs an HTTP GET request on the file `.status` at
|
A client then performs an HTTP GET request on the file `.status` at
|
||||||
@@ -558,7 +558,7 @@ a POST request to the authorisation server URL containing in its body
|
|||||||
a JSON dictionary of the following form:
|
a JSON dictionary of the following form:
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
"location": "https://skald.example.org/group/groupname/",
|
"location": "https://skald.example.org/hall/hallname/",
|
||||||
"username": username,
|
"username": username,
|
||||||
"password": password
|
"password": password
|
||||||
}
|
}
|
||||||
@@ -575,7 +575,7 @@ a signed JWT (a "JWS") the body of which has the following form:
|
|||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
"sub": username,
|
"sub": username,
|
||||||
"aud": "https://skald.example.org/group/groupname/",
|
"aud": "https://skald.example.org/hall/hallname/",
|
||||||
"permissions": ["present"],
|
"permissions": ["present"],
|
||||||
"iat": now,
|
"iat": now,
|
||||||
"exp": now + 30s,
|
"exp": now + 30s,
|
||||||
@@ -599,4 +599,4 @@ authentication portal performs authorisation, generates a token as above,
|
|||||||
then redirects back to the group's URL with the token stores in a URL
|
then redirects back to the group's URL with the token stores in a URL
|
||||||
query parameter named `token`:
|
query parameter named `token`:
|
||||||
|
|
||||||
https://skald.example.org/group/groupname/?token=eyJhbG...
|
https://skald.example.org/hall/hallname/?token=eyJhbG...
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ your browser directly at the desired group URL.
|
|||||||
### The group pages
|
### The group pages
|
||||||
|
|
||||||
A group named e.g. *city-watch* has an associated page
|
A group named e.g. *city-watch* has an associated page
|
||||||
`/group/city-watch/`. After login, it presents an interface consisting
|
`/hall/city-watch/`. After login, it presents an interface consisting
|
||||||
of three panes:
|
of three panes:
|
||||||
|
|
||||||
- the left side pane contains the list of users who have joined the
|
- the left side pane contains the list of users who have joined the
|
||||||
@@ -71,7 +71,7 @@ a *user menu*, a menu that applies to that specific user.
|
|||||||
The centre pane is a traditional chat interface, with an input form at the
|
The centre pane is a traditional chat interface, with an input form at the
|
||||||
bottom and the chat history above it. Chat history is never saved to
|
bottom and the chat history above it. Chat history is never saved to
|
||||||
disk, and is erased after four hours (or whatever is specified in the
|
disk, and is erased after four hours (or whatever is specified in the
|
||||||
`"max-history-age"` field of the group definition).
|
`"max-history-age"` field of the hall definition).
|
||||||
|
|
||||||
Double-clicking on a message opens a contextual menu.
|
Double-clicking on a message opens a contextual menu.
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ commands.
|
|||||||
In order to generate an invitation link, choose the entry *Invite user* in
|
In order to generate an invitation link, choose the entry *Invite user* in
|
||||||
the group menu. This generates a link of the form
|
the group menu. This generates a link of the form
|
||||||
|
|
||||||
https://skald.example.org:8443/group/city-watch/?token=XXX
|
https://skald.example.org:8443/hall/city-watch/?token=XXX
|
||||||
|
|
||||||
where the *XXX* part, known as the *token*, is a shared secret. Such
|
where the *XXX* part, known as the *token*, is a shared secret. Such
|
||||||
a link allows password-less login to the group, and may therefore be
|
a link allows password-less login to the group, and may therefore be
|
||||||
@@ -160,7 +160,7 @@ fields are as follows:
|
|||||||
only meaningful permission is `"admin"`;
|
only meaningful permission is `"admin"`;
|
||||||
|
|
||||||
- `writableGroups`: if true, then the API used by `skaldctl` can be used
|
- `writableGroups`: if true, then the API used by `skaldctl` can be used
|
||||||
to modify group definitions; if unset or false, then only read-only
|
to modify hall definitions; if unset or false, then only read-only
|
||||||
access is allowed;
|
access is allowed;
|
||||||
|
|
||||||
- `allowOrigin` is an array containing the list of HTTP origins that
|
- `allowOrigin` is an array containing the list of HTTP origins that
|
||||||
@@ -187,21 +187,21 @@ also be edited manually (there is no need to restart the server).
|
|||||||
|
|
||||||
#### Creating, modifying, and deleting groups
|
#### Creating, modifying, and deleting groups
|
||||||
|
|
||||||
A group is created using `skaldctl create-group`:
|
A group is created using `skaldctl create-hall`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl create-group -group city-watch
|
skaldctl create-hall -hall city-watch
|
||||||
```
|
```
|
||||||
|
|
||||||
There are a number of options to customise the behaviour of the group, see
|
There are a number of options to customise the behaviour of the group, see
|
||||||
`skaldctl create-group -help` for a full list. For example, in order to
|
`skaldctl create-hall -help` for a full list. For example, in order to
|
||||||
create a group that allows unrestricted creation of tokens, say:
|
create a hall that allows unrestricted creation of tokens, say:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl create-group -group city-watch -unrestricted-tokens
|
skaldctl create-hall -hall city-watch -unrestricted-tokens
|
||||||
```
|
```
|
||||||
|
|
||||||
For more advanced configuration, `skaldctl create-group` can be invoked
|
For more advanced configuration, `skaldctl create-hall` can be invoked
|
||||||
with the `-json` flag, in which case it takes a JSON template from standard
|
with the `-json` flag, in which case it takes a JSON template from standard
|
||||||
input. The syntax of a JSON template is just like that of a group
|
input. The syntax of a JSON template is just like that of a group
|
||||||
definition file (see below), except that it must not contain the fields
|
definition file (see below), except that it must not contain the fields
|
||||||
@@ -209,27 +209,27 @@ definition file (see below), except that it must not contain the fields
|
|||||||
(see the section *Group description reference* below):
|
(see the section *Group description reference* below):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
echo '{"redirect": "https://skald.example.org:8443/group/city-watch/"}' | skaldctl create-group -group amcw -json
|
echo '{"redirect": "https://skald.example.org:8443/hall/city-watch/"}' | skaldctl create-hall -hall amcw -json
|
||||||
```
|
```
|
||||||
|
|
||||||
Groups are modified using `skaldctl update-group`:
|
Groups are modified using `skaldctl update-hall`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl update-group -group city-watch -unrestricted-tokens=false
|
skaldctl update-hall -hall city-watch -unrestricted-tokens=false
|
||||||
```
|
```
|
||||||
|
|
||||||
If a JSON template is provided to `skaldctl update-group`, then it is
|
If a JSON template is provided to `skaldctl update-hall`, then it is
|
||||||
merged with the existing group configuration. Entries may be deleted
|
merged with the existing group configuration. Entries may be deleted
|
||||||
by setting them to `null` in the template:
|
by setting them to `null` in the template:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
echo '{"redirect": null}' | skaldctl update-group -group amcw
|
echo '{"redirect": null}' | skaldctl update-hall -hall amcw
|
||||||
```
|
```
|
||||||
|
|
||||||
A group is deleted using `skaldctl delete-group`:
|
A group is deleted using `skaldctl delete-hall`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl delete-group -group amcw
|
skaldctl delete-hall -hall amcw
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Creating, modifying, and deleting users
|
#### Creating, modifying, and deleting users
|
||||||
@@ -237,7 +237,7 @@ skaldctl delete-group -group amcw
|
|||||||
A user entry is created with the `skaldctl create-user` command:
|
A user entry is created with the `skaldctl create-user` command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl create-user -group city-watch -user vimes -permissions op
|
skaldctl create-user -hall city-watch -user vimes -permissions op
|
||||||
```
|
```
|
||||||
|
|
||||||
If the `-permissions` flag is not specified, it defaults to `present`,
|
If the `-permissions` flag is not specified, it defaults to `present`,
|
||||||
@@ -253,7 +253,7 @@ In order to be useful, a user entry needs to be assigned a password. This
|
|||||||
is done with the `skaldctl set-password` command:
|
is done with the `skaldctl set-password` command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl set-password -group city-watch -user vimes
|
skaldctl set-password -hall city-watch -user vimes
|
||||||
```
|
```
|
||||||
|
|
||||||
#### The fallback user
|
#### The fallback user
|
||||||
@@ -262,15 +262,15 @@ It is sometimes useful to allow multiple users to log in using the same
|
|||||||
password. This can be achieved by defining the *wildcard* user:
|
password. This can be achieved by defining the *wildcard* user:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl create-user -group city-watch -wildcard
|
skaldctl create-user -hall city-watch -wildcard
|
||||||
skaldctl set-password -group city-watch -wildcard
|
skaldctl set-password -hall city-watch -wildcard
|
||||||
```
|
```
|
||||||
|
|
||||||
For open groups, where any user can login with any password, the wildcard
|
For open groups, where any user can login with any password, the wildcard
|
||||||
user's password is set to the password of type `wildcard`:
|
user's password is set to the password of type `wildcard`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl set-password -group city-watch -wildcard -type wildcard
|
skaldctl set-password -hall city-watch -wildcard -type wildcard
|
||||||
```
|
```
|
||||||
|
|
||||||
See the section *Client authorisation* below for more information about
|
See the section *Client authorisation* below for more information about
|
||||||
@@ -291,7 +291,7 @@ Automatic subgroups are enabled by setting the `"auto-subgroups"`
|
|||||||
field in the group description:
|
field in the group description:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl create-group unseen-university -auto-subgroups
|
skaldctl create-hall unseen-university -auto-subgroups
|
||||||
```
|
```
|
||||||
|
|
||||||
Whenever a user attempts to access a subgroup of `unseen-university`, for
|
Whenever a user attempts to access a subgroup of `unseen-university`, for
|
||||||
@@ -308,8 +308,8 @@ the `skaldctl` utility's `create-token`, `revoke-token`, `delete-token`
|
|||||||
and `list-tokens` commands:
|
and `list-tokens` commands:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl create-token -group city-watch
|
skaldctl create-token -hall city-watch
|
||||||
skaldctl list-tokens -l -group city-watch
|
skaldctl list-tokens -l -hall city-watch
|
||||||
```
|
```
|
||||||
|
|
||||||
A token that is generated with the `-include-subgroups` flag applies to
|
A token that is generated with the `-include-subgroups` flag applies to
|
||||||
@@ -317,25 +317,25 @@ the whole hierarchy rooted at the given group, including both ordinary
|
|||||||
groups and automatically generated subgroups.
|
groups and automatically generated subgroups.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl create-token -group city-watch -include-subgroups
|
skaldctl create-token -hall city-watch -include-subgroups
|
||||||
```
|
```
|
||||||
|
|
||||||
Such a token can be attached to the root of the group hierarchy, and
|
Such a token can be attached to the root of the group hierarchy, and
|
||||||
therefore be valid for any group on the server:
|
therefore be valid for any group on the server:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
skaldctl create-token -group '' -include-subgroups
|
skaldctl create-token -hall '' -include-subgroups
|
||||||
```
|
```
|
||||||
|
|
||||||
### Group description reference
|
### Group description reference
|
||||||
|
|
||||||
The definition for the group called *groupname* is in the file
|
The definition for the group called *hallname* is in the file
|
||||||
`groups/groupname.json`; it does not contain the group name, which makes
|
`groups/hallname.json`; it does not contain the group name, which makes
|
||||||
it easy to copy or link group definitions. You may use subdirectories:
|
it easy to copy or link hall definitions. You may use subdirectories:
|
||||||
a file `groups/teaching/networking.json` defines a group called
|
a file `groups/teaching/networking.json` defines a group called
|
||||||
*teaching/networking*.
|
*teaching/networking*.
|
||||||
|
|
||||||
Every group definition file contains a single JSON dictionary. All fields
|
Every hall definition file contains a single JSON dictionary. All fields
|
||||||
are optional. The following fields are allowed:
|
are optional. The following fields are allowed:
|
||||||
|
|
||||||
- `users`: a dictionary that maps user names to user descriptions (see
|
- `users`: a dictionary that maps user names to user descriptions (see
|
||||||
@@ -352,7 +352,7 @@ are optional. The following fields are allowed:
|
|||||||
displayed at the top of the group page;
|
displayed at the top of the group page;
|
||||||
|
|
||||||
- `description`: a human-readable description of the group; this is
|
- `description`: a human-readable description of the group; this is
|
||||||
displayed on the landing page for public groups;
|
displayed on the landing page for public halls;
|
||||||
|
|
||||||
- `contact`: a human-readable contact for this group, such as an e-mail
|
- `contact`: a human-readable contact for this group, such as an e-mail
|
||||||
address, ignored by the server;
|
address, ignored by the server;
|
||||||
|
|||||||
+87
-87
@@ -65,25 +65,25 @@ var commands = map[string]command{
|
|||||||
command: deletePasswordCmd,
|
command: deletePasswordCmd,
|
||||||
description: "delete a user's password",
|
description: "delete a user's password",
|
||||||
},
|
},
|
||||||
"list-groups": {
|
"list-halls": {
|
||||||
command: listGroupsCmd,
|
command: listGroupsCmd,
|
||||||
description: "list groups",
|
description: "list halls",
|
||||||
},
|
},
|
||||||
"show-group": {
|
"show-hall": {
|
||||||
command: showGroupCmd,
|
command: showGroupCmd,
|
||||||
description: "show group definition",
|
description: "show hall definition",
|
||||||
},
|
},
|
||||||
"create-group": {
|
"create-hall": {
|
||||||
command: createGroupCmd,
|
command: createGroupCmd,
|
||||||
description: "create a group",
|
description: "create a hall",
|
||||||
},
|
},
|
||||||
"update-group": {
|
"update-hall": {
|
||||||
command: updateGroupCmd,
|
command: updateGroupCmd,
|
||||||
description: "change a group's definition",
|
description: "change a hall's definition",
|
||||||
},
|
},
|
||||||
"delete-group": {
|
"delete-hall": {
|
||||||
command: deleteGroupCmd,
|
command: deleteGroupCmd,
|
||||||
description: "delete a group",
|
description: "delete a hall",
|
||||||
},
|
},
|
||||||
"list-users": {
|
"list-users": {
|
||||||
command: listUsersCmd,
|
command: listUsersCmd,
|
||||||
@@ -593,7 +593,7 @@ func deleteValue(url string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setPasswordCmd(cmdname string, args []string) {
|
func setPasswordCmd(cmdname string, args []string) {
|
||||||
var groupname, username string
|
var hallname, username string
|
||||||
var wildcard bool
|
var wildcard bool
|
||||||
var password, algorithm string
|
var password, algorithm string
|
||||||
var iterations, cost, length, saltlen int
|
var iterations, cost, length, saltlen int
|
||||||
@@ -603,7 +603,7 @@ func setPasswordCmd(cmdname string, args []string) {
|
|||||||
"%v [option...] %v [option...]\n",
|
"%v [option...] %v [option...]\n",
|
||||||
os.Args[0], cmdname,
|
os.Args[0], cmdname,
|
||||||
)
|
)
|
||||||
cmd.StringVar(&groupname, "group", "", "group `name`")
|
cmd.StringVar(&hallname, "hall", "", "hall `name`")
|
||||||
cmd.StringVar(&username, "user", "", "user `name`")
|
cmd.StringVar(&username, "user", "", "user `name`")
|
||||||
cmd.BoolVar(&wildcard, "wildcard", false, "set wildcard user's password")
|
cmd.BoolVar(&wildcard, "wildcard", false, "set wildcard user's password")
|
||||||
cmd.StringVar(&password, "password", "", "new `password`")
|
cmd.StringVar(&password, "password", "", "new `password`")
|
||||||
@@ -622,9 +622,9 @@ func setPasswordCmd(cmdname string, args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if groupname == "" {
|
if hallname == "" {
|
||||||
fmt.Fprintf(cmd.Output(),
|
fmt.Fprintf(cmd.Output(),
|
||||||
"Option \"-group\" is required\n")
|
"Option \"-hall\" is required\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -655,12 +655,12 @@ func setPasswordCmd(cmdname string, args []string) {
|
|||||||
var u string
|
var u string
|
||||||
if wildcard {
|
if wildcard {
|
||||||
u, err = url.JoinPath(
|
u, err = url.JoinPath(
|
||||||
serverURL, "/skald-api/v0/.groups", groupname,
|
serverURL, "/skald-api/v0/.halls", hallname,
|
||||||
".wildcard-user/.password",
|
".wildcard-user/.password",
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
u, err = url.JoinPath(
|
u, err = url.JoinPath(
|
||||||
serverURL, "/skald-api/v0/.groups", groupname,
|
serverURL, "/skald-api/v0/.halls", hallname,
|
||||||
".users", username, ".password",
|
".users", username, ".password",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -675,7 +675,7 @@ func setPasswordCmd(cmdname string, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func deletePasswordCmd(cmdname string, args []string) {
|
func deletePasswordCmd(cmdname string, args []string) {
|
||||||
var groupname, username string
|
var hallname, username string
|
||||||
var wildcard bool
|
var wildcard bool
|
||||||
|
|
||||||
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
||||||
@@ -683,7 +683,7 @@ func deletePasswordCmd(cmdname string, args []string) {
|
|||||||
"%v [option...] %v [option...]\n",
|
"%v [option...] %v [option...]\n",
|
||||||
os.Args[0], cmdname,
|
os.Args[0], cmdname,
|
||||||
)
|
)
|
||||||
cmd.StringVar(&groupname, "group", "", "group `name`")
|
cmd.StringVar(&hallname, "hall", "", "hall `name`")
|
||||||
cmd.StringVar(&username, "user", "", "user `name`")
|
cmd.StringVar(&username, "user", "", "user `name`")
|
||||||
cmd.BoolVar(&wildcard, "wildcard", false, "set wildcard user's password")
|
cmd.BoolVar(&wildcard, "wildcard", false, "set wildcard user's password")
|
||||||
cmd.Parse(args)
|
cmd.Parse(args)
|
||||||
@@ -693,9 +693,9 @@ func deletePasswordCmd(cmdname string, args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if groupname == "" {
|
if hallname == "" {
|
||||||
fmt.Fprintf(cmd.Output(),
|
fmt.Fprintf(cmd.Output(),
|
||||||
"Option \"-group\" is required\n")
|
"Option \"-hall\" is required\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,12 +710,12 @@ func deletePasswordCmd(cmdname string, args []string) {
|
|||||||
var err error
|
var err error
|
||||||
if wildcard {
|
if wildcard {
|
||||||
u, err = url.JoinPath(
|
u, err = url.JoinPath(
|
||||||
serverURL, "/skald-api/v0/.groups", groupname,
|
serverURL, "/skald-api/v0/.halls", hallname,
|
||||||
".wildcard-user/.password",
|
".wildcard-user/.password",
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
u, err = url.JoinPath(
|
u, err = url.JoinPath(
|
||||||
serverURL, "/skald-api/v0/.groups", groupname,
|
serverURL, "/skald-api/v0/.halls", hallname,
|
||||||
".users", username, ".password",
|
".users", username, ".password",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -796,7 +796,7 @@ func stdinJSON(doit bool) (map[string]any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createGroupCmd(cmdname string, args []string) {
|
func createGroupCmd(cmdname string, args []string) {
|
||||||
var groupname string
|
var hallname string
|
||||||
var unrestrictedTokens, autoSubgroups boolOption
|
var unrestrictedTokens, autoSubgroups boolOption
|
||||||
var doJSON bool
|
var doJSON bool
|
||||||
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
||||||
@@ -804,7 +804,7 @@ func createGroupCmd(cmdname string, args []string) {
|
|||||||
"%v [option...] %v [option...]\n",
|
"%v [option...] %v [option...]\n",
|
||||||
os.Args[0], cmdname,
|
os.Args[0], cmdname,
|
||||||
)
|
)
|
||||||
cmd.StringVar(&groupname, "group", "", "group `name`")
|
cmd.StringVar(&hallname, "hall", "", "hall `name`")
|
||||||
cmd.Var(&unrestrictedTokens, "unrestricted-tokens",
|
cmd.Var(&unrestrictedTokens, "unrestricted-tokens",
|
||||||
"allow ordinary users to create tokens",
|
"allow ordinary users to create tokens",
|
||||||
)
|
)
|
||||||
@@ -821,14 +821,14 @@ func createGroupCmd(cmdname string, args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if groupname == "" {
|
if hallname == "" {
|
||||||
fmt.Fprintf(cmd.Output(),
|
fmt.Fprintf(cmd.Output(),
|
||||||
"Option \"-group\" is required\n")
|
"Option \"-hall\" is required\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.JoinPath(
|
u, err := url.JoinPath(
|
||||||
serverURL, "/skald-api/v0/.groups", groupname,
|
serverURL, "/skald-api/v0/.halls", hallname,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Build URL: %v", err)
|
log.Fatalf("Build URL: %v", err)
|
||||||
@@ -848,18 +848,18 @@ func createGroupCmd(cmdname string, args []string) {
|
|||||||
|
|
||||||
err = putJSON(u, data, false)
|
err = putJSON(u, data, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Create group: %v", err)
|
log.Fatalf("Create hall: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteGroupCmd(cmdname string, args []string) {
|
func deleteGroupCmd(cmdname string, args []string) {
|
||||||
var groupname string
|
var hallname string
|
||||||
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
||||||
setUsage(cmd, cmdname,
|
setUsage(cmd, cmdname,
|
||||||
"%v [option...] %v [option...]\n",
|
"%v [option...] %v [option...]\n",
|
||||||
os.Args[0], cmdname,
|
os.Args[0], cmdname,
|
||||||
)
|
)
|
||||||
cmd.StringVar(&groupname, "group", "", "group `name`")
|
cmd.StringVar(&hallname, "hall", "", "hall `name`")
|
||||||
cmd.Parse(args)
|
cmd.Parse(args)
|
||||||
|
|
||||||
if cmd.NArg() != 0 {
|
if cmd.NArg() != 0 {
|
||||||
@@ -867,14 +867,14 @@ func deleteGroupCmd(cmdname string, args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if groupname == "" {
|
if hallname == "" {
|
||||||
fmt.Fprintf(cmd.Output(),
|
fmt.Fprintf(cmd.Output(),
|
||||||
"Option \"-group\" is required\n")
|
"Option \"-hall\" is required\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.JoinPath(
|
u, err := url.JoinPath(
|
||||||
serverURL, "/skald-api/v0/.groups", groupname,
|
serverURL, "/skald-api/v0/.halls", hallname,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Build URL: %v", err)
|
log.Fatalf("Build URL: %v", err)
|
||||||
@@ -882,12 +882,12 @@ func deleteGroupCmd(cmdname string, args []string) {
|
|||||||
|
|
||||||
err = deleteValue(u)
|
err = deleteValue(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Delete group: %v", err)
|
log.Fatalf("Delete hall: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateGroupCmd(cmdname string, args []string) {
|
func updateGroupCmd(cmdname string, args []string) {
|
||||||
var groupname string
|
var hallname string
|
||||||
var unrestrictedTokens, autoSubgroups boolOption
|
var unrestrictedTokens, autoSubgroups boolOption
|
||||||
var doJSON bool
|
var doJSON bool
|
||||||
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
||||||
@@ -895,7 +895,7 @@ func updateGroupCmd(cmdname string, args []string) {
|
|||||||
"%v [option...] %v [option...]\n",
|
"%v [option...] %v [option...]\n",
|
||||||
os.Args[0], cmdname,
|
os.Args[0], cmdname,
|
||||||
)
|
)
|
||||||
cmd.StringVar(&groupname, "group", "", "group `name`")
|
cmd.StringVar(&hallname, "hall", "", "hall `name`")
|
||||||
cmd.Var(&unrestrictedTokens, "unrestricted-tokens",
|
cmd.Var(&unrestrictedTokens, "unrestricted-tokens",
|
||||||
"allow ordinary users to create tokens",
|
"allow ordinary users to create tokens",
|
||||||
)
|
)
|
||||||
@@ -913,7 +913,7 @@ func updateGroupCmd(cmdname string, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.JoinPath(
|
u, err := url.JoinPath(
|
||||||
serverURL, "/skald-api/v0/.groups", groupname,
|
serverURL, "/skald-api/v0/.halls", hallname,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Build URL: %v", err)
|
log.Fatalf("Build URL: %v", err)
|
||||||
@@ -1007,25 +1007,25 @@ func match(patterns []string, value string) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func listUsersCmd(cmdname string, args []string) {
|
func listUsersCmd(cmdname string, args []string) {
|
||||||
var groupname string
|
var hallname string
|
||||||
var long bool
|
var long bool
|
||||||
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
||||||
setUsage(cmd, cmdname,
|
setUsage(cmd, cmdname,
|
||||||
"%v [option...] %v [option...] [pattern...]\n",
|
"%v [option...] %v [option...] [pattern...]\n",
|
||||||
os.Args[0], cmdname,
|
os.Args[0], cmdname,
|
||||||
)
|
)
|
||||||
cmd.StringVar(&groupname, "group", "", "group `name`")
|
cmd.StringVar(&hallname, "hall", "", "hall `name`")
|
||||||
cmd.BoolVar(&long, "l", false, "display permissions")
|
cmd.BoolVar(&long, "l", false, "display permissions")
|
||||||
cmd.Parse(args)
|
cmd.Parse(args)
|
||||||
patterns := cmd.Args()
|
patterns := cmd.Args()
|
||||||
|
|
||||||
if groupname == "" {
|
if hallname == "" {
|
||||||
fmt.Fprintf(cmd.Output(),
|
fmt.Fprintf(cmd.Output(),
|
||||||
"Option \"-group\" is required\n")
|
"Option \"-hall\" is required\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.JoinPath(serverURL, "/skald-api/v0/.groups/", groupname,
|
u, err := url.JoinPath(serverURL, "/skald-api/v0/.halls/", hallname,
|
||||||
".users/")
|
".users/")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Build URL: %v", err)
|
log.Fatalf("Build URL: %v", err)
|
||||||
@@ -1069,21 +1069,21 @@ func listUsersCmd(cmdname string, args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func userURL(wildcard bool, groupname, username string) (string, error) {
|
func userURL(wildcard bool, hallname, username string) (string, error) {
|
||||||
if wildcard {
|
if wildcard {
|
||||||
return url.JoinPath(
|
return url.JoinPath(
|
||||||
serverURL, "/skald-api/v0/.groups", groupname,
|
serverURL, "/skald-api/v0/.halls", hallname,
|
||||||
".wildcard-user",
|
".wildcard-user",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return url.JoinPath(
|
return url.JoinPath(
|
||||||
serverURL, "/skald-api/v0/.groups", groupname,
|
serverURL, "/skald-api/v0/.halls", hallname,
|
||||||
".users", username,
|
".users", username,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createUserCmd(cmdname string, args []string) {
|
func createUserCmd(cmdname string, args []string) {
|
||||||
var groupname, username string
|
var hallname, username string
|
||||||
var wildcard bool
|
var wildcard bool
|
||||||
var permissions stringOption
|
var permissions stringOption
|
||||||
var doJSON bool
|
var doJSON bool
|
||||||
@@ -1092,7 +1092,7 @@ func createUserCmd(cmdname string, args []string) {
|
|||||||
"%v [option...] %v [option...]\n",
|
"%v [option...] %v [option...]\n",
|
||||||
os.Args[0], cmdname,
|
os.Args[0], cmdname,
|
||||||
)
|
)
|
||||||
cmd.StringVar(&groupname, "group", "", "group `name`")
|
cmd.StringVar(&hallname, "hall", "", "hall `name`")
|
||||||
cmd.StringVar(&username, "user", "", "user `name`")
|
cmd.StringVar(&username, "user", "", "user `name`")
|
||||||
cmd.BoolVar(&wildcard, "wildcard", false, "create the wildcard user")
|
cmd.BoolVar(&wildcard, "wildcard", false, "create the wildcard user")
|
||||||
cmd.Var(&permissions, "permissions",
|
cmd.Var(&permissions, "permissions",
|
||||||
@@ -1107,9 +1107,9 @@ func createUserCmd(cmdname string, args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if groupname == "" {
|
if hallname == "" {
|
||||||
fmt.Fprintf(cmd.Output(),
|
fmt.Fprintf(cmd.Output(),
|
||||||
"Option \"-group\" is required\n")
|
"Option \"-hall\" is required\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1131,7 +1131,7 @@ func createUserCmd(cmdname string, args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := userURL(wildcard, groupname, username)
|
u, err := userURL(wildcard, hallname, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Build URL: %v", err)
|
log.Fatalf("Build URL: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1155,7 +1155,7 @@ func createUserCmd(cmdname string, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateUserCmd(cmdname string, args []string) {
|
func updateUserCmd(cmdname string, args []string) {
|
||||||
var groupname, username string
|
var hallname, username string
|
||||||
var wildcard bool
|
var wildcard bool
|
||||||
var permissions stringOption
|
var permissions stringOption
|
||||||
var doJSON bool
|
var doJSON bool
|
||||||
@@ -1164,7 +1164,7 @@ func updateUserCmd(cmdname string, args []string) {
|
|||||||
"%v [option...] %v [option...]\n",
|
"%v [option...] %v [option...]\n",
|
||||||
os.Args[0], cmdname,
|
os.Args[0], cmdname,
|
||||||
)
|
)
|
||||||
cmd.StringVar(&groupname, "group", "", "group `name`")
|
cmd.StringVar(&hallname, "hall", "", "hall `name`")
|
||||||
cmd.StringVar(&username, "user", "", "user `name`")
|
cmd.StringVar(&username, "user", "", "user `name`")
|
||||||
cmd.BoolVar(&wildcard, "wildcard", false, "update the wildcard user")
|
cmd.BoolVar(&wildcard, "wildcard", false, "update the wildcard user")
|
||||||
cmd.Var(&permissions, "permissions", "permissions")
|
cmd.Var(&permissions, "permissions", "permissions")
|
||||||
@@ -1175,7 +1175,7 @@ func updateUserCmd(cmdname string, args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := userURL(wildcard, groupname, username)
|
u, err := userURL(wildcard, hallname, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Build URL: %v", err)
|
log.Fatalf("Build URL: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1217,13 +1217,13 @@ func updateUserCmd(cmdname string, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func deleteUserCmd(cmdname string, args []string) {
|
func deleteUserCmd(cmdname string, args []string) {
|
||||||
var groupname, username string
|
var hallname, username string
|
||||||
var wildcard bool
|
var wildcard bool
|
||||||
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
||||||
setUsage(cmd, cmdname, "%v [option...] %v [option...]\n",
|
setUsage(cmd, cmdname, "%v [option...] %v [option...]\n",
|
||||||
os.Args[0], cmdname,
|
os.Args[0], cmdname,
|
||||||
)
|
)
|
||||||
cmd.StringVar(&groupname, "group", "", "group `name`")
|
cmd.StringVar(&hallname, "hall", "", "hall `name`")
|
||||||
cmd.StringVar(&username, "user", "", "user `name`")
|
cmd.StringVar(&username, "user", "", "user `name`")
|
||||||
cmd.BoolVar(&wildcard, "wildcard", false, "delete the wildcard user")
|
cmd.BoolVar(&wildcard, "wildcard", false, "delete the wildcard user")
|
||||||
cmd.Parse(args)
|
cmd.Parse(args)
|
||||||
@@ -1233,9 +1233,9 @@ func deleteUserCmd(cmdname string, args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if groupname == "" {
|
if hallname == "" {
|
||||||
fmt.Fprintf(cmd.Output(),
|
fmt.Fprintf(cmd.Output(),
|
||||||
"Option \"-group\" is required\n")
|
"Option \"-hall\" is required\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1250,12 +1250,12 @@ func deleteUserCmd(cmdname string, args []string) {
|
|||||||
var err error
|
var err error
|
||||||
if wildcard {
|
if wildcard {
|
||||||
u, err = url.JoinPath(
|
u, err = url.JoinPath(
|
||||||
serverURL, "/skald-api/v0/.groups", groupname,
|
serverURL, "/skald-api/v0/.halls", hallname,
|
||||||
".wildcard-user",
|
".wildcard-user",
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
u, err = url.JoinPath(
|
u, err = url.JoinPath(
|
||||||
serverURL, "/skald-api/v0/.groups", groupname,
|
serverURL, "/skald-api/v0/.halls", hallname,
|
||||||
".users", username,
|
".users", username,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1270,13 +1270,13 @@ func deleteUserCmd(cmdname string, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func showGroupCmd(cmdname string, args []string) {
|
func showGroupCmd(cmdname string, args []string) {
|
||||||
var groupname string
|
var hallname string
|
||||||
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
||||||
setUsage(cmd, cmdname,
|
setUsage(cmd, cmdname,
|
||||||
"%v [option...] %v\n",
|
"%v [option...] %v\n",
|
||||||
os.Args[0], cmdname,
|
os.Args[0], cmdname,
|
||||||
)
|
)
|
||||||
cmd.StringVar(&groupname, "group", "", "group `name`")
|
cmd.StringVar(&hallname, "hall", "", "hall `name`")
|
||||||
cmd.Parse(args)
|
cmd.Parse(args)
|
||||||
|
|
||||||
if cmd.NArg() != 0 {
|
if cmd.NArg() != 0 {
|
||||||
@@ -1284,11 +1284,11 @@ func showGroupCmd(cmdname string, args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if groupname == "" {
|
if hallname == "" {
|
||||||
log.Fatal("Option \"-group\" is required.")
|
log.Fatal("Option \"-hall\" is required.")
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.JoinPath(serverURL, "/skald-api/v0/.groups/", groupname)
|
u, err := url.JoinPath(serverURL, "/skald-api/v0/.halls/", hallname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Build URL: %v", err)
|
log.Fatalf("Build URL: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1296,7 +1296,7 @@ func showGroupCmd(cmdname string, args []string) {
|
|||||||
var description map[string]any
|
var description map[string]any
|
||||||
_, err = getJSON(u, &description)
|
_, err = getJSON(u, &description)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Get group description: %v", err)
|
log.Fatalf("Get hall description: %v", err)
|
||||||
}
|
}
|
||||||
encoder := json.NewEncoder(os.Stdout)
|
encoder := json.NewEncoder(os.Stdout)
|
||||||
encoder.SetIndent("", " ")
|
encoder.SetIndent("", " ")
|
||||||
@@ -1315,7 +1315,7 @@ func listGroupsCmd(cmdname string, args []string) {
|
|||||||
cmd.Parse(args)
|
cmd.Parse(args)
|
||||||
patterns := cmd.Args()
|
patterns := cmd.Args()
|
||||||
|
|
||||||
u, err := url.JoinPath(serverURL, "/skald-api/v0/.groups/")
|
u, err := url.JoinPath(serverURL, "/skald-api/v0/.halls/")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Build URL: %v", err)
|
log.Fatalf("Build URL: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1323,7 +1323,7 @@ func listGroupsCmd(cmdname string, args []string) {
|
|||||||
var groups []string
|
var groups []string
|
||||||
_, err = getJSON(u, &groups)
|
_, err = getJSON(u, &groups)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Get groups: %v", err)
|
log.Fatalf("Get halls: %v", err)
|
||||||
}
|
}
|
||||||
sort.Slice(groups, func(i, j int) bool {
|
sort.Slice(groups, func(i, j int) bool {
|
||||||
return groups[i] < groups[j]
|
return groups[i] < groups[j]
|
||||||
@@ -1343,13 +1343,13 @@ func listGroupsCmd(cmdname string, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func listTokensCmd(cmdname string, args []string) {
|
func listTokensCmd(cmdname string, args []string) {
|
||||||
var groupname stringOption
|
var hallname stringOption
|
||||||
var long bool
|
var long bool
|
||||||
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
||||||
setUsage(cmd, cmdname, "%v [option...] %v [option...]\n",
|
setUsage(cmd, cmdname, "%v [option...] %v [option...]\n",
|
||||||
os.Args[0], cmdname,
|
os.Args[0], cmdname,
|
||||||
)
|
)
|
||||||
cmd.Var(&groupname, "group", "group `name`")
|
cmd.Var(&hallname, "hall", "hall `name`")
|
||||||
cmd.BoolVar(&long, "l", false, "display token fields")
|
cmd.BoolVar(&long, "l", false, "display token fields")
|
||||||
cmd.Parse(args)
|
cmd.Parse(args)
|
||||||
|
|
||||||
@@ -1358,14 +1358,14 @@ func listTokensCmd(cmdname string, args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !groupname.set {
|
if !hallname.set {
|
||||||
fmt.Fprintf(cmd.Output(),
|
fmt.Fprintf(cmd.Output(),
|
||||||
"Option \"-group\" is required\n")
|
"Option \"-hall\" is required\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.JoinPath(
|
u, err := url.JoinPath(
|
||||||
serverURL, "/skald-api/v0/.groups/", groupname.value, ".tokens/",
|
serverURL, "/skald-api/v0/.halls/", hallname.value, ".tokens/",
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1430,14 +1430,14 @@ func listTokensCmd(cmdname string, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createTokenCmd(cmdname string, args []string) {
|
func createTokenCmd(cmdname string, args []string) {
|
||||||
var groupname stringOption
|
var hallname stringOption
|
||||||
var username, permissions string
|
var username, permissions string
|
||||||
var includeSubgroups boolOption
|
var includeSubgroups boolOption
|
||||||
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
||||||
setUsage(cmd, cmdname, "%v [option...] %v [option...]\n",
|
setUsage(cmd, cmdname, "%v [option...] %v [option...]\n",
|
||||||
os.Args[0], cmdname,
|
os.Args[0], cmdname,
|
||||||
)
|
)
|
||||||
cmd.Var(&groupname, "group", "group `name`")
|
cmd.Var(&hallname, "hall", "hall `name`")
|
||||||
cmd.Var(&includeSubgroups, "include-subgroups", "include subgroups")
|
cmd.Var(&includeSubgroups, "include-subgroups", "include subgroups")
|
||||||
cmd.StringVar(&username, "user", "", "encode user `name` in token")
|
cmd.StringVar(&username, "user", "", "encode user `name` in token")
|
||||||
cmd.StringVar(&permissions, "permissions", "present", "permissions")
|
cmd.StringVar(&permissions, "permissions", "present", "permissions")
|
||||||
@@ -1448,9 +1448,9 @@ func createTokenCmd(cmdname string, args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !groupname.set {
|
if !hallname.set {
|
||||||
fmt.Fprintf(cmd.Output(),
|
fmt.Fprintf(cmd.Output(),
|
||||||
"Option \"-group\" is required\n")
|
"Option \"-hall\" is required\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1469,7 +1469,7 @@ func createTokenCmd(cmdname string, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.JoinPath(
|
u, err := url.JoinPath(
|
||||||
serverURL, "/skald-api/v0/.groups/", groupname.value, ".tokens/",
|
serverURL, "/skald-api/v0/.halls/", hallname.value, ".tokens/",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Build URL: %v", err)
|
log.Fatalf("Build URL: %v", err)
|
||||||
@@ -1483,13 +1483,13 @@ func createTokenCmd(cmdname string, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func revokeTokenCmd(cmdname string, args []string) {
|
func revokeTokenCmd(cmdname string, args []string) {
|
||||||
var groupname stringOption
|
var hallname stringOption
|
||||||
var token string
|
var token string
|
||||||
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
||||||
setUsage(cmd, cmdname, "%v [option...] %v [option...]\n",
|
setUsage(cmd, cmdname, "%v [option...] %v [option...]\n",
|
||||||
os.Args[0], cmdname,
|
os.Args[0], cmdname,
|
||||||
)
|
)
|
||||||
cmd.Var(&groupname, "group", "group `name`")
|
cmd.Var(&hallname, "hall", "hall `name`")
|
||||||
cmd.StringVar(&token, "token", "", "`token` to delete")
|
cmd.StringVar(&token, "token", "", "`token` to delete")
|
||||||
cmd.Parse(args)
|
cmd.Parse(args)
|
||||||
|
|
||||||
@@ -1498,14 +1498,14 @@ func revokeTokenCmd(cmdname string, args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !groupname.set || token == "" {
|
if !hallname.set || token == "" {
|
||||||
fmt.Fprintf(cmd.Output(),
|
fmt.Fprintf(cmd.Output(),
|
||||||
"Options \"-group\" and \"-token\" are required\n")
|
"Options \"-hall\" and \"-token\" are required\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.JoinPath(
|
u, err := url.JoinPath(
|
||||||
serverURL, "/skald-api/v0/.groups/", groupname.value,
|
serverURL, "/skald-api/v0/.halls/", hallname.value,
|
||||||
".tokens", token,
|
".tokens", token,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1522,13 +1522,13 @@ func revokeTokenCmd(cmdname string, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func deleteTokenCmd(cmdname string, args []string) {
|
func deleteTokenCmd(cmdname string, args []string) {
|
||||||
var groupname stringOption
|
var hallname stringOption
|
||||||
var token string
|
var token string
|
||||||
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
cmd := flag.NewFlagSet(cmdname, flag.ExitOnError)
|
||||||
setUsage(cmd, cmdname, "%v [option...] %v [option...]\n",
|
setUsage(cmd, cmdname, "%v [option...] %v [option...]\n",
|
||||||
os.Args[0], cmdname,
|
os.Args[0], cmdname,
|
||||||
)
|
)
|
||||||
cmd.Var(&groupname, "group", "group `name`")
|
cmd.Var(&hallname, "hall", "hall `name`")
|
||||||
cmd.StringVar(&token, "token", "", "`token` to delete")
|
cmd.StringVar(&token, "token", "", "`token` to delete")
|
||||||
cmd.Parse(args)
|
cmd.Parse(args)
|
||||||
|
|
||||||
@@ -1537,14 +1537,14 @@ func deleteTokenCmd(cmdname string, args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !groupname.set || token == "" {
|
if !hallname.set || token == "" {
|
||||||
fmt.Fprintf(cmd.Output(),
|
fmt.Fprintf(cmd.Output(),
|
||||||
"Options \"-group\" and \"-token\" are required\n")
|
"Options \"-hall\" and \"-token\" are required\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.JoinPath(
|
u, err := url.JoinPath(
|
||||||
serverURL, "/skald-api/v0/.groups/", groupname.value,
|
serverURL, "/skald-api/v0/.halls/", hallname.value,
|
||||||
".tokens", token,
|
".tokens", token,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ document.getElementById('start').onclick = async function(e) {
|
|||||||
let button = /** @type{HTMLButtonElement} */(this);
|
let button = /** @type{HTMLButtonElement} */(this);
|
||||||
button.hidden = true;
|
button.hidden = true;
|
||||||
try {
|
try {
|
||||||
await start("/group/public/");
|
await start("/hall/public/");
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
displayError(e);
|
displayError(e);
|
||||||
};
|
};
|
||||||
|
|||||||
+7
-7
@@ -15,22 +15,22 @@
|
|||||||
<div class="home">
|
<div class="home">
|
||||||
<h1 id="title" class="navbar-brand">Skald</h1>
|
<h1 id="title" class="navbar-brand">Skald</h1>
|
||||||
|
|
||||||
<form id="groupform">
|
<form id="hallform">
|
||||||
<label for="group">Group:</label>
|
<label for="hall">Hall:</label>
|
||||||
<input id="group" type="text" name="group" class="form-control form-control-inline" autofocus/>
|
<input id="hall" type="text" name="hall" class="form-control form-control-inline" autofocus/>
|
||||||
<input id='submitbutton' type="submit" value="Join" class="btn btn-default btn-large"/><br/>
|
<input id='submitbutton' type="submit" value="Join" class="btn btn-default btn-large"/><br/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p id="errormessage"></p>
|
<p id="errormessage"></p>
|
||||||
|
|
||||||
<div id="public-groups" class="groups">
|
<div id="public-halls" class="groups">
|
||||||
<h2>Public groups</h2>
|
<h2>Public halls</h2>
|
||||||
|
|
||||||
<table id="public-groups-table"></table>
|
<table id="public-halls-table"></table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="signature">
|
<footer class="signature">
|
||||||
<p><a href="https://skald.org/">Skald</a> by <a href="https://www.irif.fr/~jch/" rel="author">Juliusz Chroboczek</a></p>
|
<p>Skald is derived from <a href="https://galene.org/">Galene</a> by <a href="https://www.irif.fr/~jch/" rel="author">Juliusz Chroboczek</a></p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="/mainpage.js" defer></script>
|
<script src="/mainpage.js" defer></script>
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ body {
|
|||||||
padding: 1.875rem;
|
padding: 1.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#public-groups-table tr a{
|
#public-halls-table tr a{
|
||||||
margin-left: 0.9375rem;
|
margin-left: 0.9375rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -20,16 +20,16 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
document.getElementById('groupform').onsubmit = async function(e) {
|
document.getElementById('hallform').onsubmit = async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
clearError();
|
clearError();
|
||||||
let groupinput = document.getElementById('group')
|
let groupinput = document.getElementById('hall')
|
||||||
let button = document.getElementById('submitbutton');
|
let button = document.getElementById('submitbutton');
|
||||||
|
|
||||||
let group = groupinput.value.trim();
|
let group = groupinput.value.trim();
|
||||||
if(group === '')
|
if(group === '')
|
||||||
return;
|
return;
|
||||||
let url = '/group/' + group + '/';
|
let url = '/hall/' + group + '/';
|
||||||
let statusUrl = url + '.status.json';
|
let statusUrl = url + '.status.json';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -78,25 +78,25 @@ function clearError() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listPublicGroups() {
|
async function listPublicHalls() {
|
||||||
let div = document.getElementById('public-groups');
|
let div = document.getElementById('public-halls');
|
||||||
let table = document.getElementById('public-groups-table');
|
let table = document.getElementById('public-halls-table');
|
||||||
|
|
||||||
let l;
|
let l;
|
||||||
try {
|
try {
|
||||||
let r = await fetch('/public-groups.json');
|
let r = await fetch('/public-halls.json');
|
||||||
if(!r.ok)
|
if(!r.ok)
|
||||||
throw new Error(`${r.status} ${r.statusText}`);
|
throw new Error(`${r.status} ${r.statusText}`);
|
||||||
l = await r.json();
|
l = await r.json();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
table.textContent = `Couldn't fetch groups: ${e}`;
|
table.textContent = `Couldn't fetch halls: ${e}`;
|
||||||
div.classList.remove('nogroups');
|
div.classList.remove('nogroups');
|
||||||
div.classList.add('groups');
|
div.classList.add('groups');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (l.length === 0) {
|
if (l.length === 0) {
|
||||||
table.textContent = '(No groups found.)';
|
table.textContent = '(No halls found.)';
|
||||||
div.classList.remove('groups');
|
div.classList.remove('groups');
|
||||||
div.classList.add('nogroups');
|
div.classList.add('nogroups');
|
||||||
return;
|
return;
|
||||||
@@ -131,4 +131,4 @@ async function listPublicGroups() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
listPublicGroups();
|
listPublicHalls();
|
||||||
|
|||||||
+14
-14
@@ -159,7 +159,7 @@ async function updateObject(url, values, etag) {
|
|||||||
* @returns {Promise<Array<string>>}
|
* @returns {Promise<Array<string>>}
|
||||||
*/
|
*/
|
||||||
async function listGroups() {
|
async function listGroups() {
|
||||||
return await listObjects('/skald-api/v0/.groups/');
|
return await listObjects('/skald-api/v0/.halls/');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,7 +170,7 @@ async function listGroups() {
|
|||||||
* @returns {Promise<Object>}
|
* @returns {Promise<Object>}
|
||||||
*/
|
*/
|
||||||
async function getGroup(group, etag) {
|
async function getGroup(group, etag) {
|
||||||
return await getObject(`/skald-api/v0/.groups/${group}`, etag);
|
return await getObject(`/skald-api/v0/.halls/${group}`, etag);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -180,7 +180,7 @@ async function getGroup(group, etag) {
|
|||||||
* @param {Object} [values]
|
* @param {Object} [values]
|
||||||
*/
|
*/
|
||||||
async function createGroup(group, values) {
|
async function createGroup(group, values) {
|
||||||
return await createObject(`/skald-api/v0/.groups/${group}`, values);
|
return await createObject(`/skald-api/v0/.halls/${group}`, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -190,11 +190,11 @@ async function createGroup(group, values) {
|
|||||||
* @param {string} [etag]
|
* @param {string} [etag]
|
||||||
*/
|
*/
|
||||||
async function deleteGroup(group, etag) {
|
async function deleteGroup(group, etag) {
|
||||||
return await deleteObject(`/skald-api/v0/.groups/${group}`, etag);
|
return await deleteObject(`/skald-api/v0/.halls/${group}`, etag);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* updateGroup modifies a group definition.
|
* updateGroup modifies a hall definition.
|
||||||
* Any fields present in values are overriden, any fields absent in values
|
* Any fields present in values are overriden, any fields absent in values
|
||||||
* are left unchanged.
|
* are left unchanged.
|
||||||
*
|
*
|
||||||
@@ -203,7 +203,7 @@ async function deleteGroup(group, etag) {
|
|||||||
* @param {string} [etag]
|
* @param {string} [etag]
|
||||||
*/
|
*/
|
||||||
async function updateGroup(group, values, etag) {
|
async function updateGroup(group, values, etag) {
|
||||||
return await updateObject(`/skald-api/v0/.groups/${group}`, values);
|
return await updateObject(`/skald-api/v0/.halls/${group}`, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -213,7 +213,7 @@ async function updateGroup(group, values, etag) {
|
|||||||
* @returns {Promise<Array<string>>}
|
* @returns {Promise<Array<string>>}
|
||||||
*/
|
*/
|
||||||
async function listUsers(group) {
|
async function listUsers(group) {
|
||||||
return await listObjects(`/skald-api/v0/.groups/${group}/.users/`);
|
return await listObjects(`/skald-api/v0/.halls/${group}/.users/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -225,11 +225,11 @@ async function listUsers(group) {
|
|||||||
*/
|
*/
|
||||||
function userURL(group, user, wildcard) {
|
function userURL(group, user, wildcard) {
|
||||||
if(wildcard)
|
if(wildcard)
|
||||||
return `/skald-api/v0/.groups/${group}/.wildcard-user`;
|
return `/skald-api/v0/.halls/${group}/.wildcard-user`;
|
||||||
else if(user === "")
|
else if(user === "")
|
||||||
return `/skald-api/v0/.groups/${group}/.empty-user`;
|
return `/skald-api/v0/.halls/${group}/.empty-user`;
|
||||||
else
|
else
|
||||||
return `/skald-api/v0/.groups/${group}/.users/${user}`
|
return `/skald-api/v0/.halls/${group}/.users/${user}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -320,7 +320,7 @@ async function setPassword(group, user, wildcard, password, oldpassword) {
|
|||||||
* @returns {Promise<Array<string>>}
|
* @returns {Promise<Array<string>>}
|
||||||
*/
|
*/
|
||||||
async function listTokens(group) {
|
async function listTokens(group) {
|
||||||
return await listObjects(`/skald-api/v0/.groups/${group}/.tokens/`);
|
return await listObjects(`/skald-api/v0/.halls/${group}/.tokens/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -332,7 +332,7 @@ async function listTokens(group) {
|
|||||||
* @returns {Promise<Object>}
|
* @returns {Promise<Object>}
|
||||||
*/
|
*/
|
||||||
async function getToken(group, token, etag) {
|
async function getToken(group, token, etag) {
|
||||||
return await getObject(`/skald-api/v0/.groups/${group}/.tokens/${token}`,
|
return await getObject(`/skald-api/v0/.halls/${group}/.tokens/${token}`,
|
||||||
etag);
|
etag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,7 +353,7 @@ async function createToken(group, template) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let r = await fetch(
|
let r = await fetch(
|
||||||
`/skald-api/v0/.groups/${group}/.tokens/`,
|
`/skald-api/v0/.halls/${group}/.tokens/`,
|
||||||
options);
|
options);
|
||||||
if(!r.ok)
|
if(!r.ok)
|
||||||
throw httpError(r);
|
throw httpError(r);
|
||||||
@@ -373,6 +373,6 @@ async function updateToken(group, token, etag) {
|
|||||||
if(!token.token)
|
if(!token.token)
|
||||||
throw new Error("Unnamed token");
|
throw new Error("Unnamed token");
|
||||||
return await updateObject(
|
return await updateObject(
|
||||||
`/skald-api/v0/.groups/${group}/.tokens/${token.token}`,
|
`/skald-api/v0/.halls/${group}/.tokens/${token.token}`,
|
||||||
token, etag);
|
token, etag);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,20 +83,20 @@ X Search for remaining Galene names and classify each survivor as attribution/hi
|
|||||||
# PHASE 3: URLS, API, PROTOCOL, AND CONTROL TOOL
|
# PHASE 3: URLS, API, PROTOCOL, AND CONTROL TOOL
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
- Rename browser hall URLs from /group/name/ to /hall/name/
|
X Rename browser hall URLs from /group/name/ to /hall/name/
|
||||||
- Rename /public-groups.json to /public-halls.json
|
X Rename /public-groups.json to /public-halls.json
|
||||||
- Rename /galene-api/ to /skald-api/
|
X Rename /galene-api/ to /skald-api/
|
||||||
- Rename API collection paths from .groups to .halls
|
X Rename API collection paths from .groups to .halls
|
||||||
- Rename protocol join fields from group to hall
|
- Rename protocol join fields from group to hall
|
||||||
- Rename protocol callbacks/events from joined group semantics to joined hall semantics
|
- Rename protocol callbacks/events from joined group semantics to joined hall semantics
|
||||||
- Update static/protocol.js public API: ServerConnection.join and related callbacks
|
- Update static/protocol.js public API: ServerConnection.join and related callbacks
|
||||||
- Update static/management.js to use /skald-api/v0/.halls/
|
X Update static/management.js to use /skald-api/v0/.halls/
|
||||||
- Update static/mainpage.js to open /hall/ URLs and fetch /public-halls.json
|
X Update static/mainpage.js to open /hall/ URLs and fetch /public-halls.json
|
||||||
- Update stats endpoint naming only if it exposes Skald/group terminology
|
- Update stats endpoint naming only if it exposes Skald/group terminology
|
||||||
- Update skaldctl command names: create-hall, update-hall, delete-hall, list-halls
|
X Update skaldctl command names: create-hall, update-hall, delete-hall, list-halls
|
||||||
- Update skaldctl flags: -hall, -include-subhalls, -auto-subhalls, etc.
|
- Update skaldctl flags: -hall, -include-subhalls, -auto-subhalls, etc.
|
||||||
- Update skaldctl help text and error messages
|
X Update skaldctl help text and error messages
|
||||||
- Update API and webserver tests for /hall/ and /skald-api/
|
X Update API and webserver tests for /hall/ and /skald-api/
|
||||||
- Verify old /group/, /public-groups.json, /galene-api/, and .groups paths fail clearly
|
- Verify old /group/, /public-groups.json, /galene-api/, and .groups paths fail clearly
|
||||||
- Verify protocol backward compatibility with Skald is intentionally broken
|
- Verify protocol backward compatibility with Skald is intentionally broken
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -190,10 +190,10 @@ func parseJWT(token string, keys []map[string]any) (*JWT, error) {
|
|||||||
|
|
||||||
func matchGroup(pth, group string, includeSubgroups bool) bool {
|
func matchGroup(pth, group string, includeSubgroups bool) bool {
|
||||||
if !includeSubgroups {
|
if !includeSubgroups {
|
||||||
return pth == "/group/"+group+"/"
|
return pth == "/hall/"+group+"/"
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(pth, "/group/") {
|
if !strings.HasPrefix(pth, "/hall/") {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ func matchGroup(pth, group string, includeSubgroups bool) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.HasPrefix("/group/"+group+"/", pth)
|
return strings.HasPrefix("/hall/"+group+"/", pth)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (token *JWT) Check(host, group string, username *string) (string, []string, error) {
|
func (token *JWT) Check(host, group string, username *string) (string, []string, error) {
|
||||||
|
|||||||
+13
-13
@@ -85,21 +85,21 @@ func TestMatchGroup(t *testing.T) {
|
|||||||
s bool
|
s bool
|
||||||
}
|
}
|
||||||
good := []tt{
|
good := []tt{
|
||||||
{"/group/a/", "a", false},
|
{"/hall/a/", "a", false},
|
||||||
{"/group/a/b/", "a/b", false},
|
{"/hall/a/b/", "a/b", false},
|
||||||
{"/group/a/", "a", true},
|
{"/hall/a/", "a", true},
|
||||||
{"/group/a/", "a/b", true},
|
{"/hall/a/", "a/b", true},
|
||||||
{"/group/a/b/", "a/b", true},
|
{"/hall/a/b/", "a/b", true},
|
||||||
{"/group/a/b/", "a/b/c", true},
|
{"/hall/a/b/", "a/b/c", true},
|
||||||
}
|
}
|
||||||
|
|
||||||
bad := []tt{
|
bad := []tt{
|
||||||
{"/group/a/", "b", false},
|
{"/hall/a/", "b", false},
|
||||||
{"/group/a", "a", false},
|
{"/hall/a", "a", false},
|
||||||
{"/group/a", "a", true},
|
{"/hall/a", "a", true},
|
||||||
{"/group/a/", "a/b", false},
|
{"/hall/a/", "a/b", false},
|
||||||
{"/group/a/b/", "a", false},
|
{"/hall/a/b/", "a", false},
|
||||||
{"/group/a/b/", "a", true},
|
{"/hall/a/b/", "a", true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range good {
|
for _, test := range good {
|
||||||
@@ -142,7 +142,7 @@ func TestJWT(t *testing.T) {
|
|||||||
john := "john"
|
john := "john"
|
||||||
jack := "jack"
|
jack := "jack"
|
||||||
baseClaims := jwt.MapClaims{
|
baseClaims := jwt.MapClaims{
|
||||||
"aud": "https://skald.org:8443/group/auth/",
|
"aud": "https://skald.org:8443/hall/auth/",
|
||||||
"permissions": []string{"present"},
|
"permissions": []string{"present"},
|
||||||
"iat": int64(1645310294),
|
"iat": int64(1645310294),
|
||||||
"exp": int64(2906750294),
|
"exp": int64(2906750294),
|
||||||
|
|||||||
+16
-16
@@ -46,27 +46,27 @@ func TestStatefulCheck(t *testing.T) {
|
|||||||
user2 := "user2"
|
user2 := "user2"
|
||||||
token1 := &Stateful{
|
token1 := &Stateful{
|
||||||
Token: "token",
|
Token: "token",
|
||||||
Group: "group",
|
Group: "hall",
|
||||||
Username: &user,
|
Username: &user,
|
||||||
Permissions: []string{"present", "message"},
|
Permissions: []string{"present", "message"},
|
||||||
Expires: &future,
|
Expires: &future,
|
||||||
}
|
}
|
||||||
token2 := &Stateful{
|
token2 := &Stateful{
|
||||||
Token: "token",
|
Token: "token",
|
||||||
Group: "group",
|
Group: "hall",
|
||||||
Permissions: []string{"present", "message"},
|
Permissions: []string{"present", "message"},
|
||||||
Expires: &future,
|
Expires: &future,
|
||||||
}
|
}
|
||||||
token3 := &Stateful{
|
token3 := &Stateful{
|
||||||
Token: "token",
|
Token: "token",
|
||||||
Group: "group",
|
Group: "hall",
|
||||||
Username: &user,
|
Username: &user,
|
||||||
Permissions: []string{"present", "message"},
|
Permissions: []string{"present", "message"},
|
||||||
Expires: &past,
|
Expires: &past,
|
||||||
}
|
}
|
||||||
token4 := &Stateful{
|
token4 := &Stateful{
|
||||||
Token: "token",
|
Token: "token",
|
||||||
Group: "group",
|
Group: "hall",
|
||||||
Username: &user,
|
Username: &user,
|
||||||
Permissions: []string{"present", "message"},
|
Permissions: []string{"present", "message"},
|
||||||
Expires: &future,
|
Expires: &future,
|
||||||
@@ -74,7 +74,7 @@ func TestStatefulCheck(t *testing.T) {
|
|||||||
}
|
}
|
||||||
token5 := &Stateful{
|
token5 := &Stateful{
|
||||||
Token: "token",
|
Token: "token",
|
||||||
Group: "group",
|
Group: "hall",
|
||||||
IncludeSubgroups: true,
|
IncludeSubgroups: true,
|
||||||
Username: &user,
|
Username: &user,
|
||||||
Permissions: []string{"present", "message"},
|
Permissions: []string{"present", "message"},
|
||||||
@@ -106,48 +106,48 @@ func TestStatefulCheck(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
token: token1,
|
token: token1,
|
||||||
group: "group",
|
group: "hall",
|
||||||
username: &user,
|
username: &user,
|
||||||
expUsername: user,
|
expUsername: user,
|
||||||
expPermissions: []string{"present", "message"},
|
expPermissions: []string{"present", "message"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
token: token1,
|
token: token1,
|
||||||
group: "group",
|
group: "hall",
|
||||||
username: &user2,
|
username: &user2,
|
||||||
expUsername: user,
|
expUsername: user,
|
||||||
expPermissions: []string{"present", "message"},
|
expPermissions: []string{"present", "message"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
token: token1,
|
token: token1,
|
||||||
group: "group",
|
group: "hall",
|
||||||
expUsername: user,
|
expUsername: user,
|
||||||
expPermissions: []string{"present", "message"},
|
expPermissions: []string{"present", "message"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
token: token2,
|
token: token2,
|
||||||
group: "group",
|
group: "hall",
|
||||||
username: &user,
|
username: &user,
|
||||||
expUsername: "",
|
expUsername: "",
|
||||||
expPermissions: []string{"present", "message"},
|
expPermissions: []string{"present", "message"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
token: token5,
|
token: token5,
|
||||||
group: "group",
|
group: "hall",
|
||||||
username: &user,
|
username: &user,
|
||||||
expUsername: "user",
|
expUsername: "user",
|
||||||
expPermissions: []string{"present", "message"},
|
expPermissions: []string{"present", "message"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
token: token5,
|
token: token5,
|
||||||
group: "group/subgroup",
|
group: "hall/subhall",
|
||||||
username: &user,
|
username: &user,
|
||||||
expUsername: "user",
|
expUsername: "user",
|
||||||
expPermissions: []string{"present", "message"},
|
expPermissions: []string{"present", "message"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
token: token7,
|
token: token7,
|
||||||
group: "group/subgroup",
|
group: "hall/subhall",
|
||||||
username: &user,
|
username: &user,
|
||||||
expUsername: "user",
|
expUsername: "user",
|
||||||
expPermissions: []string{"present", "message"},
|
expPermissions: []string{"present", "message"},
|
||||||
@@ -171,17 +171,17 @@ func TestStatefulCheck(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
token: token1,
|
token: token1,
|
||||||
group: "group2",
|
group: "hall2",
|
||||||
username: &user,
|
username: &user,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
token: token3,
|
token: token3,
|
||||||
group: "group",
|
group: "hall",
|
||||||
username: &user,
|
username: &user,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
token: token4,
|
token: token4,
|
||||||
group: "group",
|
group: "hall",
|
||||||
username: &user,
|
username: &user,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -196,7 +196,7 @@ func TestStatefulCheck(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
token: token6,
|
token: token6,
|
||||||
group: "group",
|
group: "hall",
|
||||||
username: &user,
|
username: &user,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-2
@@ -25,7 +25,7 @@ func TestToken(t *testing.T) {
|
|||||||
user := "user"
|
user := "user"
|
||||||
_, err = Update(&Stateful{
|
_, err = Update(&Stateful{
|
||||||
Token: "token",
|
Token: "token",
|
||||||
Group: "group",
|
Group: "hall",
|
||||||
Username: &user,
|
Username: &user,
|
||||||
Permissions: []string{"present"},
|
Permissions: []string{"present"},
|
||||||
Expires: &future,
|
Expires: &future,
|
||||||
@@ -39,7 +39,7 @@ func TestToken(t *testing.T) {
|
|||||||
t.Fatalf("Parse: %v", err)
|
t.Fatalf("Parse: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err = token.Check("skald.org:8443", "group", &user)
|
_, _, err = token.Check("skald.org:8443", "hall", &user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Check: %v", err)
|
t.Errorf("Check: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -34,7 +34,7 @@ func checkAdmin(w http.ResponseWriter, r *http.Request) bool {
|
|||||||
// checkPasswordAdmin checks whether the client authentifies as either an
|
// checkPasswordAdmin checks whether the client authentifies as either an
|
||||||
// administrator or the given user. It is used to check whether the
|
// administrator or the given user. It is used to check whether the
|
||||||
// client has the right to change user's password.
|
// client has the right to change user's password.
|
||||||
func checkPasswordAdmin(w http.ResponseWriter, r *http.Request, groupname, user string, wildcard bool) bool {
|
func checkPasswordAdmin(w http.ResponseWriter, r *http.Request, hallname, user string, wildcard bool) bool {
|
||||||
username, password, ok := r.BasicAuth()
|
username, password, ok := r.BasicAuth()
|
||||||
if ok {
|
if ok {
|
||||||
ok, err := adminMatch(username, password)
|
ok, err := adminMatch(username, password)
|
||||||
@@ -47,11 +47,11 @@ func checkPasswordAdmin(w http.ResponseWriter, r *http.Request, groupname, user
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ok && !wildcard && username == user {
|
if ok && !wildcard && username == user {
|
||||||
desc, err := group.GetDescription(groupname)
|
desc, err := group.GetDescription(hallname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalError(w,
|
internalError(w,
|
||||||
"Get description for group %v: %v",
|
"Get description for group %v: %v",
|
||||||
groupname, err,
|
hallname, err,
|
||||||
)
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -169,7 +169,7 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
w.Header().Set("cache-control", "no-cache")
|
w.Header().Set("cache-control", "no-cache")
|
||||||
sendJSON(w, r, stats.GetGroups())
|
sendJSON(w, r, stats.GetGroups())
|
||||||
case ".groups":
|
case ".halls":
|
||||||
apiGroupHandler(w, r, rest)
|
apiGroupHandler(w, r, rest)
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|||||||
+59
-59
@@ -115,58 +115,58 @@ func TestApi(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var groups []string
|
var groups []string
|
||||||
err = getJSON("/skald-api/v0/.groups/", &groups)
|
err = getJSON("/skald-api/v0/.halls/", &groups)
|
||||||
if err != nil || len(groups) != 0 {
|
if err != nil || len(groups) != 0 {
|
||||||
t.Errorf("Get groups: %v", err)
|
t.Errorf("Get halls: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := do("PUT", "/skald-api/v0/.groups/test/",
|
resp, err := do("PUT", "/skald-api/v0/.halls/test/",
|
||||||
"application/json", "\"foo\"", "",
|
"application/json", "\"foo\"", "",
|
||||||
"{}")
|
"{}")
|
||||||
if err != nil || resp.StatusCode != http.StatusPreconditionFailed {
|
if err != nil || resp.StatusCode != http.StatusPreconditionFailed {
|
||||||
t.Errorf("Create group (bad ETag): %v %v", err, resp.StatusCode)
|
t.Errorf("Create hall (bad ETag): %v %v", err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("PUT", "/skald-api/v0/.groups/test/",
|
resp, err = do("PUT", "/skald-api/v0/.halls/test/",
|
||||||
"text/plain", "", "",
|
"text/plain", "", "",
|
||||||
"Hello, world!")
|
"Hello, world!")
|
||||||
if err != nil || resp.StatusCode != http.StatusUnsupportedMediaType {
|
if err != nil || resp.StatusCode != http.StatusUnsupportedMediaType {
|
||||||
t.Errorf("Create group (bad content-type): %v %v",
|
t.Errorf("Create hall (bad content-type): %v %v",
|
||||||
err, resp.StatusCode)
|
err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("PUT", "/skald-api/v0/.groups/test/",
|
resp, err = do("PUT", "/skald-api/v0/.halls/test/",
|
||||||
"application/json", "", "*",
|
"application/json", "", "*",
|
||||||
"{}")
|
"{}")
|
||||||
if err != nil || resp.StatusCode != http.StatusCreated {
|
if err != nil || resp.StatusCode != http.StatusCreated {
|
||||||
t.Errorf("Create group: %v %v", err, resp.StatusCode)
|
t.Errorf("Create hall: %v %v", err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
var desc *group.Description
|
var desc *group.Description
|
||||||
err = getJSON("/skald-api/v0/.groups/test/", &desc)
|
err = getJSON("/skald-api/v0/.halls/test/", &desc)
|
||||||
if err != nil || len(desc.Users) != 0 {
|
if err != nil || len(desc.Users) != 0 {
|
||||||
t.Errorf("Get group: %v", err)
|
t.Errorf("Get hall: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("PUT", "/skald-api/v0/.groups/test/",
|
resp, err = do("PUT", "/skald-api/v0/.halls/test/",
|
||||||
"application/json", "", "*",
|
"application/json", "", "*",
|
||||||
"{}")
|
"{}")
|
||||||
if err != nil || resp.StatusCode != http.StatusPreconditionFailed {
|
if err != nil || resp.StatusCode != http.StatusPreconditionFailed {
|
||||||
t.Errorf("Create group (bad ETag): %v %v", err, resp.StatusCode)
|
t.Errorf("Create hall (bad ETag): %v %v", err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("DELETE", "/skald-api/v0/.groups/test/",
|
resp, err = do("DELETE", "/skald-api/v0/.halls/test/",
|
||||||
"", "", "*", "")
|
"", "", "*", "")
|
||||||
if err != nil || resp.StatusCode != http.StatusPreconditionFailed {
|
if err != nil || resp.StatusCode != http.StatusPreconditionFailed {
|
||||||
t.Errorf("Delete group (bad ETag): %v %v", err, resp.StatusCode)
|
t.Errorf("Delete hall (bad ETag): %v %v", err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = getJSON("/skald-api/v0/.groups/", &groups)
|
err = getJSON("/skald-api/v0/.halls/", &groups)
|
||||||
if err != nil || len(groups) != 1 || groups[0] != "test" {
|
if err != nil || len(groups) != 1 || groups[0] != "test" {
|
||||||
t.Errorf("Get groups: %v %v", err, groups)
|
t.Errorf("Get halls: %v %v", err, groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("PUT", "/skald-api/v0/.groups/test/.keys",
|
resp, err = do("PUT", "/skald-api/v0/.halls/test/.keys",
|
||||||
"application/jwk-set+json", "", "",
|
"application/jwk-set+json", "", "",
|
||||||
`{"keys": [{
|
`{"keys": [{
|
||||||
"kty": "oct", "alg": "HS256",
|
"kty": "oct", "alg": "HS256",
|
||||||
@@ -176,12 +176,12 @@ func TestApi(t *testing.T) {
|
|||||||
t.Errorf("Set key: %v %v", err, resp.StatusCode)
|
t.Errorf("Set key: %v %v", err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = getJSON("/skald-api/v0/.groups/test/.users/", &groups)
|
err = getJSON("/skald-api/v0/.halls/test/.users/", &groups)
|
||||||
if err != nil || len(groups) != 0 {
|
if err != nil || len(groups) != 0 {
|
||||||
t.Errorf("Get users: %v", err)
|
t.Errorf("Get users: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("PUT", "/skald-api/v0/.groups/test/.users/jch",
|
resp, err = do("PUT", "/skald-api/v0/.halls/test/.users/jch",
|
||||||
"text/plain", "", "*",
|
"text/plain", "", "*",
|
||||||
`hello, world!`)
|
`hello, world!`)
|
||||||
if err != nil || resp.StatusCode != http.StatusUnsupportedMediaType {
|
if err != nil || resp.StatusCode != http.StatusUnsupportedMediaType {
|
||||||
@@ -189,7 +189,7 @@ func TestApi(t *testing.T) {
|
|||||||
err, resp.StatusCode)
|
err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("PUT", "/skald-api/v0/.groups/test/.users/jch",
|
resp, err = do("PUT", "/skald-api/v0/.halls/test/.users/jch",
|
||||||
"application/json", "", "*",
|
"application/json", "", "*",
|
||||||
`{"permissions": "present"}`)
|
`{"permissions": "present"}`)
|
||||||
if err != nil || resp.StatusCode != http.StatusCreated {
|
if err != nil || resp.StatusCode != http.StatusCreated {
|
||||||
@@ -197,26 +197,26 @@ func TestApi(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var users []string
|
var users []string
|
||||||
err = getJSON("/skald-api/v0/.groups/test/.users/", &users)
|
err = getJSON("/skald-api/v0/.halls/test/.users/", &users)
|
||||||
if err != nil || len(users) != 1 || users[0] != "jch" {
|
if err != nil || len(users) != 1 || users[0] != "jch" {
|
||||||
t.Errorf("Get users: %v %v", err, users)
|
t.Errorf("Get users: %v %v", err, users)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("PUT", "/skald-api/v0/.groups/test/.users/jch",
|
resp, err = do("PUT", "/skald-api/v0/.halls/test/.users/jch",
|
||||||
"application/json", "", "*",
|
"application/json", "", "*",
|
||||||
`{"permissions": "present"}`)
|
`{"permissions": "present"}`)
|
||||||
if err != nil || resp.StatusCode != http.StatusPreconditionFailed {
|
if err != nil || resp.StatusCode != http.StatusPreconditionFailed {
|
||||||
t.Errorf("Create user (bad ETag): %v %v", err, resp.StatusCode)
|
t.Errorf("Create user (bad ETag): %v %v", err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("PUT", "/skald-api/v0/.groups/test/.users/jch/.password",
|
resp, err = do("PUT", "/skald-api/v0/.halls/test/.users/jch/.password",
|
||||||
"application/json", "", "",
|
"application/json", "", "",
|
||||||
`"toto"`)
|
`"toto"`)
|
||||||
if err != nil || resp.StatusCode != http.StatusNoContent {
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
||||||
t.Errorf("Set password (PUT): %v %v", err, resp.StatusCode)
|
t.Errorf("Set password (PUT): %v %v", err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("POST", "/skald-api/v0/.groups/test/.users/jch/.password",
|
resp, err = do("POST", "/skald-api/v0/.halls/test/.users/jch/.password",
|
||||||
"text/plain", "", "",
|
"text/plain", "", "",
|
||||||
`toto`)
|
`toto`)
|
||||||
if err != nil || resp.StatusCode != http.StatusNoContent {
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
||||||
@@ -224,7 +224,7 @@ func TestApi(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var user group.UserDescription
|
var user group.UserDescription
|
||||||
err = getJSON("/skald-api/v0/.groups/test/.users/jch", &user)
|
err = getJSON("/skald-api/v0/.halls/test/.users/jch", &user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Get user: %v", err)
|
t.Errorf("Get user: %v", err)
|
||||||
}
|
}
|
||||||
@@ -245,10 +245,10 @@ func TestApi(t *testing.T) {
|
|||||||
t.Errorf("Password.Type: %v", desc.Users["jch"].Password.Type)
|
t.Errorf("Password.Type: %v", desc.Users["jch"].Password.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("DELETE", "/skald-api/v0/.groups/test/.users/jch",
|
resp, err = do("DELETE", "/skald-api/v0/.halls/test/.users/jch",
|
||||||
"", "", "", "")
|
"", "", "", "")
|
||||||
if err != nil || resp.StatusCode != http.StatusNoContent {
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
||||||
t.Errorf("Delete group: %v %v", err, resp.StatusCode)
|
t.Errorf("Delete hall: %v %v", err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
desc, err = group.GetDescription("test")
|
desc, err = group.GetDescription("test")
|
||||||
@@ -260,14 +260,14 @@ func TestApi(t *testing.T) {
|
|||||||
t.Errorf("Users (after delete): %#v", desc.Users)
|
t.Errorf("Users (after delete): %#v", desc.Users)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("PUT", "/skald-api/v0/.groups/test/.wildcard-user",
|
resp, err = do("PUT", "/skald-api/v0/.halls/test/.wildcard-user",
|
||||||
"application/json", "", "*",
|
"application/json", "", "*",
|
||||||
`{"permissions": "present"}`)
|
`{"permissions": "present"}`)
|
||||||
if err != nil || resp.StatusCode != http.StatusCreated {
|
if err != nil || resp.StatusCode != http.StatusCreated {
|
||||||
t.Errorf("Create wildcard user: %v %v", err, resp.StatusCode)
|
t.Errorf("Create wildcard user: %v %v", err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = getJSON("/skald-api/v0/.groups/test/.wildcard-user", &user)
|
err = getJSON("/skald-api/v0/.halls/test/.wildcard-user", &user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Get wildcard user: %v", err)
|
t.Errorf("Get wildcard user: %v", err)
|
||||||
}
|
}
|
||||||
@@ -281,7 +281,7 @@ func TestApi(t *testing.T) {
|
|||||||
t.Errorf("Got %v, expected %v", desc.WildcardUser, user)
|
t.Errorf("Got %v, expected %v", desc.WildcardUser, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("DELETE", "/skald-api/v0/.groups/test/.wildcard-user",
|
resp, err = do("DELETE", "/skald-api/v0/.halls/test/.wildcard-user",
|
||||||
"", "", "", "")
|
"", "", "", "")
|
||||||
if err != nil || resp.StatusCode != http.StatusNoContent {
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
||||||
t.Errorf("Delete wildcard user: %v %v", err, resp.StatusCode)
|
t.Errorf("Delete wildcard user: %v %v", err, resp.StatusCode)
|
||||||
@@ -300,33 +300,33 @@ func TestApi(t *testing.T) {
|
|||||||
t.Errorf("Keys: %v", len(desc.AuthKeys))
|
t.Errorf("Keys: %v", len(desc.AuthKeys))
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("POST", "/skald-api/v0/.groups/test/.tokens/",
|
resp, err = do("POST", "/skald-api/v0/.halls/test/.tokens/",
|
||||||
"application/json", "", "", `{"group":"bad"}`)
|
"application/json", "", "", `{"group":"bad"}`)
|
||||||
if err != nil || resp.StatusCode != http.StatusBadRequest {
|
if err != nil || resp.StatusCode != http.StatusBadRequest {
|
||||||
t.Errorf("Create token (bad group): %v %v", err, resp.StatusCode)
|
t.Errorf("Create token (bad group): %v %v", err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("POST", "/skald-api/v0/.groups/test/.tokens/",
|
resp, err = do("POST", "/skald-api/v0/.halls/test/.tokens/",
|
||||||
"application/json", "", "", "{}")
|
"application/json", "", "", "{}")
|
||||||
if err != nil || resp.StatusCode != http.StatusCreated {
|
if err != nil || resp.StatusCode != http.StatusCreated {
|
||||||
t.Errorf("Create token: %v %v", err, resp.StatusCode)
|
t.Errorf("Create token: %v %v", err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("POST", "/skald-api/v0/.groups/.tokens/",
|
resp, err = do("POST", "/skald-api/v0/.halls/.tokens/",
|
||||||
"application/json", "", "", "{\"includeSubgroups\": true}")
|
"application/json", "", "", "{\"includeSubgroups\": true}")
|
||||||
if err != nil || resp.StatusCode != http.StatusCreated {
|
if err != nil || resp.StatusCode != http.StatusCreated {
|
||||||
t.Errorf("Create global token: %v %v", err, resp.StatusCode)
|
t.Errorf("Create global token: %v %v", err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
var toknames []string
|
var toknames []string
|
||||||
err = getJSON("/skald-api/v0/.groups/test/.tokens/", &toknames)
|
err = getJSON("/skald-api/v0/.halls/test/.tokens/", &toknames)
|
||||||
if err != nil || len(toknames) != 1 {
|
if err != nil || len(toknames) != 1 {
|
||||||
t.Errorf("Get tokens: %v %v", err, toknames)
|
t.Errorf("Get tokens: %v %v", err, toknames)
|
||||||
}
|
}
|
||||||
tokname := toknames[0]
|
tokname := toknames[0]
|
||||||
|
|
||||||
var globalToknames []string
|
var globalToknames []string
|
||||||
err = getJSON("/skald-api/v0/.groups/test/.tokens/", &globalToknames)
|
err = getJSON("/skald-api/v0/.halls/test/.tokens/", &globalToknames)
|
||||||
if err != nil || len(globalToknames) != 1 {
|
if err != nil || len(globalToknames) != 1 {
|
||||||
t.Errorf("Get global tokens: %v %v", err, globalToknames)
|
t.Errorf("Get global tokens: %v %v", err, globalToknames)
|
||||||
}
|
}
|
||||||
@@ -336,7 +336,7 @@ func TestApi(t *testing.T) {
|
|||||||
t.Errorf("token.List: %v %v", tokens, err)
|
t.Errorf("token.List: %v %v", tokens, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenpath := "/skald-api/v0/.groups/test/.tokens/" + tokname
|
tokenpath := "/skald-api/v0/.halls/test/.tokens/" + tokname
|
||||||
var tok token.Stateful
|
var tok token.Stateful
|
||||||
err = getJSON(tokenpath, &tok)
|
err = getJSON(tokenpath, &tok)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -375,13 +375,13 @@ func TestApi(t *testing.T) {
|
|||||||
t.Errorf("Got %v, expected %v (%v)", tok.Expires, e, err)
|
t.Errorf("Got %v, expected %v (%v)", tok.Expires, e, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("PUT", "/skald-api/v0/.groups/test2",
|
resp, err = do("PUT", "/skald-api/v0/.halls/test2",
|
||||||
"application/json", "", "*", "{}")
|
"application/json", "", "*", "{}")
|
||||||
if err != nil || resp.StatusCode != http.StatusCreated {
|
if err != nil || resp.StatusCode != http.StatusCreated {
|
||||||
t.Errorf("Create test2: %v %v", err, resp.StatusCode)
|
t.Errorf("Create test2: %v %v", err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenpath2 := "/skald-api/v0/.groups/test2/.tokens/" + tokname
|
tokenpath2 := "/skald-api/v0/.halls/test2/.tokens/" + tokname
|
||||||
resp, err = do("GET", tokenpath2, "", "", "", "")
|
resp, err = do("GET", tokenpath2, "", "", "", "")
|
||||||
if err != nil || resp.StatusCode != http.StatusNotFound {
|
if err != nil || resp.StatusCode != http.StatusNotFound {
|
||||||
t.Errorf("Get token in bad group: %v %v", err, resp.StatusCode)
|
t.Errorf("Get token in bad group: %v %v", err, resp.StatusCode)
|
||||||
@@ -402,16 +402,16 @@ func TestApi(t *testing.T) {
|
|||||||
t.Errorf("Token list: %v %v", tokens, err)
|
t.Errorf("Token list: %v %v", tokens, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("DELETE", "/skald-api/v0/.groups/test/.keys",
|
resp, err = do("DELETE", "/skald-api/v0/.halls/test/.keys",
|
||||||
"", "", "", "")
|
"", "", "", "")
|
||||||
if err != nil || resp.StatusCode != http.StatusNoContent {
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
||||||
t.Errorf("Delete keys: %v %v", err, resp.StatusCode)
|
t.Errorf("Delete keys: %v %v", err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = do("DELETE", "/skald-api/v0/.groups/test/",
|
resp, err = do("DELETE", "/skald-api/v0/.halls/test/",
|
||||||
"", "", "", "")
|
"", "", "", "")
|
||||||
if err != nil || resp.StatusCode != http.StatusNoContent {
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
||||||
t.Errorf("Delete group: %v %v", err, resp.StatusCode)
|
t.Errorf("Delete hall: %v %v", err, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = group.GetDescription("test")
|
_, err = group.GetDescription("test")
|
||||||
@@ -448,8 +448,8 @@ func TestApiBadAuth(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
do("GET", "/skald-api/v0/.stats")
|
do("GET", "/skald-api/v0/.stats")
|
||||||
do("GET", "/skald-api/v0/.groups/")
|
do("GET", "/skald-api/v0/.halls/")
|
||||||
do("PUT", "/skald-api/v0/.groups/test/")
|
do("PUT", "/skald-api/v0/.halls/test/")
|
||||||
|
|
||||||
f, err := os.Create(filepath.Join(group.Directory, "test.json"))
|
f, err := os.Create(filepath.Join(group.Directory, "test.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -460,20 +460,20 @@ func TestApiBadAuth(t *testing.T) {
|
|||||||
}\n`)
|
}\n`)
|
||||||
f.Close()
|
f.Close()
|
||||||
|
|
||||||
do("PUT", "/skald-api/v0/.groups/test/")
|
do("PUT", "/skald-api/v0/.halls/test/")
|
||||||
do("DELETE", "/skald-api/v0/.groups/test/")
|
do("DELETE", "/skald-api/v0/.halls/test/")
|
||||||
do("GET", "/skald-api/v0/.groups/test/.users/")
|
do("GET", "/skald-api/v0/.halls/test/.users/")
|
||||||
do("GET", "/skald-api/v0/.groups/test/.users/jch")
|
do("GET", "/skald-api/v0/.halls/test/.users/jch")
|
||||||
do("GET", "/skald-api/v0/.groups/test/.users/jch")
|
do("GET", "/skald-api/v0/.halls/test/.users/jch")
|
||||||
do("PUT", "/skald-api/v0/.groups/test/.users/jch")
|
do("PUT", "/skald-api/v0/.halls/test/.users/jch")
|
||||||
do("DELETE", "/skald-api/v0/.groups/test/.users/jch")
|
do("DELETE", "/skald-api/v0/.halls/test/.users/jch")
|
||||||
do("GET", "/skald-api/v0/.groups/test/.users/not-jch")
|
do("GET", "/skald-api/v0/.halls/test/.users/not-jch")
|
||||||
do("PUT", "/skald-api/v0/.groups/test/.users/not-jch")
|
do("PUT", "/skald-api/v0/.halls/test/.users/not-jch")
|
||||||
do("PUT", "/skald-api/v0/.groups/test/.users/jch/.password")
|
do("PUT", "/skald-api/v0/.halls/test/.users/jch/.password")
|
||||||
do("POST", "/skald-api/v0/.groups/test/.users/jch/.password")
|
do("POST", "/skald-api/v0/.halls/test/.users/jch/.password")
|
||||||
do("GET", "/skald-api/v0/.groups/test/.tokens/")
|
do("GET", "/skald-api/v0/.halls/test/.tokens/")
|
||||||
do("POST", "/skald-api/v0/.groups/test/.tokens/")
|
do("POST", "/skald-api/v0/.halls/test/.tokens/")
|
||||||
do("GET", "/skald-api/v0/.groups/test/.tokens/token")
|
do("GET", "/skald-api/v0/.halls/test/.tokens/token")
|
||||||
do("PUT", "/skald-api/v0/.groups/test/.tokens/token")
|
do("PUT", "/skald-api/v0/.halls/test/.tokens/token")
|
||||||
do("DELETE", "/skald-api/v0/.groups/test/.tokens/token")
|
do("DELETE", "/skald-api/v0/.halls/test/.tokens/token")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ var Insecure bool
|
|||||||
|
|
||||||
func Serve(address string, dataDir string) error {
|
func Serve(address string, dataDir string) error {
|
||||||
http.Handle("/", &fileHandler{http.Dir(StaticRoot)})
|
http.Handle("/", &fileHandler{http.Dir(StaticRoot)})
|
||||||
http.HandleFunc("/group/", groupHandler)
|
http.HandleFunc("/hall/", groupHandler)
|
||||||
http.HandleFunc("/recordings",
|
http.HandleFunc("/recordings",
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r,
|
http.Redirect(w, r,
|
||||||
@@ -43,7 +43,7 @@ func Serve(address string, dataDir string) error {
|
|||||||
})
|
})
|
||||||
http.HandleFunc("/recordings/", recordingsHandler)
|
http.HandleFunc("/recordings/", recordingsHandler)
|
||||||
http.HandleFunc("/ws", wsHandler)
|
http.HandleFunc("/ws", wsHandler)
|
||||||
http.HandleFunc("/public-groups.json", publicHandler)
|
http.HandleFunc("/public-halls.json", publicHandler)
|
||||||
http.HandleFunc("/skald-api/", apiHandler)
|
http.HandleFunc("/skald-api/", apiHandler)
|
||||||
|
|
||||||
s := &http.Server{
|
s := &http.Server{
|
||||||
@@ -346,7 +346,7 @@ func groupHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
name := parseGroupName("/group/", r.URL.Path)
|
name := parseGroupName("/hall/", r.URL.Path)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
notFound(w)
|
notFound(w)
|
||||||
return
|
return
|
||||||
@@ -358,8 +358,8 @@ func groupHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.URL.Path != "/group/"+name+"/" {
|
if r.URL.Path != "/hall/"+name+"/" {
|
||||||
http.Redirect(w, r, "/group/"+name+"/",
|
http.Redirect(w, r, "/hall/"+name+"/",
|
||||||
http.StatusPermanentRedirect)
|
http.StatusPermanentRedirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -415,7 +415,7 @@ func groupStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
internalError(w, "groupStatusHandler: this shouldn't happen")
|
internalError(w, "groupStatusHandler: this shouldn't happen")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
name := parseGroupName("/group/", pth)
|
name := parseGroupName("/hall/", pth)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
notFound(w)
|
notFound(w)
|
||||||
return
|
return
|
||||||
@@ -705,13 +705,13 @@ func handleGroupAction(w http.ResponseWriter, r *http.Request, group string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkGroupPermissions(w http.ResponseWriter, r *http.Request, groupname string) bool {
|
func checkGroupPermissions(w http.ResponseWriter, r *http.Request, hallname string) bool {
|
||||||
user, pass, ok := r.BasicAuth()
|
user, pass, ok := r.BasicAuth()
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
g := group.Get(groupname)
|
g := group.Get(hallname)
|
||||||
if g == nil {
|
if g == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,17 +19,17 @@ func TestParseGroupName(t *testing.T) {
|
|||||||
{"/foo", ""},
|
{"/foo", ""},
|
||||||
{"foo", ""},
|
{"foo", ""},
|
||||||
{"group/foo", ""},
|
{"group/foo", ""},
|
||||||
{"/group", ""},
|
{"/hall", ""},
|
||||||
{"/group/..", ""},
|
{"/hall/..", ""},
|
||||||
{"/group/foo/../bar", "bar"},
|
{"/hall/foo/../bar", "bar"},
|
||||||
{"/group/foo", "foo"},
|
{"/hall/foo", "foo"},
|
||||||
{"/group/foo/", "foo"},
|
{"/hall/foo/", "foo"},
|
||||||
{"/group/foo/bar", "foo/bar"},
|
{"/hall/foo/bar", "foo/bar"},
|
||||||
{"/group/foo/bar/", "foo/bar"},
|
{"/hall/foo/bar/", "foo/bar"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, pg := range a {
|
for _, pg := range a {
|
||||||
g := parseGroupName("/group/", pg.p)
|
g := parseGroupName("/hall/", pg.p)
|
||||||
if g != pg.g {
|
if g != pg.g {
|
||||||
t.Errorf("Path %v, got %v, expected %v",
|
t.Errorf("Path %v, got %v, expected %v",
|
||||||
pg.p, g, pg.g)
|
pg.p, g, pg.g)
|
||||||
|
|||||||
+2
-2
@@ -153,7 +153,7 @@ func whipEndpointHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
name := parseGroupName("/group/", pth)
|
name := parseGroupName("/hall/", pth)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
notFound(w)
|
notFound(w)
|
||||||
return
|
return
|
||||||
@@ -269,7 +269,7 @@ func whipResourceHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
name := parseGroupName("/group/", pth)
|
name := parseGroupName("/hall/", pth)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
notFound(w)
|
notFound(w)
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user