239 lines
5.8 KiB
Erlang
239 lines
5.8 KiB
Erlang
%
|
|
% Copyright (c) 2024 Andrea Biscuola <a@abiscuola.com>
|
|
%
|
|
% Permission to use, copy, modify, and distribute this software for any
|
|
% purpose with or without fee is hereby granted, provided that the above
|
|
% copyright notice and this permission notice appear in all copies.
|
|
%
|
|
% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
%
|
|
-module(dudeswave_user_handler).
|
|
-moduledoc """
|
|
JSON API to manage users.
|
|
|
|
If the session is not valid, all the requests will return `403 Forbidden` to
|
|
the client. In case a technical problem occurs, `500 Internal Server Error`
|
|
is returned.
|
|
|
|
This module accepts four methods:
|
|
|
|
- GET /api/v1/user
|
|
Retrieve user's details. However, this call requires the user to have
|
|
a valid cookie set. Not suitable for a public page.
|
|
|
|
- POST /api/v1/user
|
|
Update user's details, like their name, description and whatnot.
|
|
|
|
- DELETE /api/v1/user
|
|
Remove a user forever. The data is delete immediately. However,
|
|
it's content is left up there. Probably a specific option will be added
|
|
later. This request does not have a body. The call deletes the user
|
|
immediately, however we return `202 Accepted` in case of success,
|
|
for the simple reason that we may make the call asynchronous
|
|
to remove additional content in background.
|
|
|
|
- PUT /api/v1/user
|
|
Register a user. The registration takes only three parameter: username,
|
|
password and e-mail. The e-mail is required if a confirmation message
|
|
is to be sent. The plan is to have a separate process handle this, so the
|
|
API just need to set the proper value for the user's `status` in it's metadata.
|
|
|
|
JSON APIs
|
|
|
|
GET /api/v1/user
|
|
|
|
Response body:
|
|
|
|
```
|
|
{
|
|
"user" : "foo",
|
|
"email": "foo@example.com",
|
|
"description": "A wonderful user",
|
|
"name": "Fantastic Foo"
|
|
}
|
|
```
|
|
|
|
Response codes:
|
|
|
|
- 200 OK (Success)
|
|
- 404 Not Found
|
|
|
|
PUT /api/v1/user
|
|
|
|
```
|
|
{
|
|
"username": "foo",
|
|
"email": "foo@example.com",
|
|
"password": "123456"
|
|
}
|
|
```
|
|
|
|
Response codes:
|
|
|
|
- 201 Created
|
|
- 400 Bad Request
|
|
- 409 Conflict (User already exists)
|
|
|
|
POST /api/v1/user
|
|
|
|
```
|
|
{
|
|
"email": "foo@example.com",
|
|
"description": "A wonderful user",
|
|
"name": "Fantastic Foo"
|
|
}
|
|
```
|
|
|
|
Response codes:
|
|
|
|
- 200 OK
|
|
- 400 Bad Request
|
|
- 404 Not Found
|
|
|
|
DELETE /api/v1/user
|
|
|
|
Response codes:
|
|
|
|
- 202 Accepted
|
|
- 404 Not Found
|
|
|
|
""".
|
|
|
|
-behaviour(cowboy_handler).
|
|
|
|
-export([init/2, terminate/3]).
|
|
|
|
%
|
|
% Callbacks exports
|
|
%
|
|
-export([allowed_methods/2, content_types_provided/2,
|
|
content_types_accepted/2, known_methods/2, is_authorized/2,
|
|
forbidden/2, resource_exists/2, is_conflict/2, previously_existed/2,
|
|
allow_missing_post/2, delete_resource/2, create_user/2, modify_user/2,
|
|
delete_completed/2, user_details/2]).
|
|
|
|
%
|
|
% Cowboy standard callbacks
|
|
%
|
|
|
|
init(Req, State) ->
|
|
{cowboy_rest, Req, State}.
|
|
|
|
known_methods(Req, State) ->
|
|
{[<<"POST">>, <<"PUT">>, <<"DELETE">>, <<"GET">>], Req, State}.
|
|
|
|
allowed_methods(Req, State) ->
|
|
{[<<"POST">>, <<"PUT">>, <<"DELETE">>, <<"GET">>], Req, State}.
|
|
|
|
is_authorized(Req, State) -> {true, Req, State}.
|
|
|
|
forbidden(Req, State) ->
|
|
case cowboy_req:method(Req) of
|
|
<<"PUT">> ->
|
|
{false, Req, State};
|
|
_ ->
|
|
{User, Auth} = dudeswave_auth:auth_cookies(Req),
|
|
|
|
case dudeswave_auth:authenticate(User, {cookie, Auth}) of
|
|
{error, service_unavailable} -> {true, Req, State};
|
|
true -> {false, Req, State};
|
|
false -> {true, Req, State}
|
|
end
|
|
end.
|
|
|
|
content_types_provided(Req, State) ->
|
|
case cowboy_req:method(Req) of
|
|
<<"PUT">> ->
|
|
{[{<<"application/json">>, create_user}], Req, State};
|
|
<<"POST">> ->
|
|
{[{<<"application/json">>, modify_user}], Req, State};
|
|
<<"DELETE">> ->
|
|
{[{<<"application/json">>, delete_user}], Req, State};
|
|
<<"GET">> ->
|
|
{[{<<"application/json">>, user_details}], Req, State}
|
|
end.
|
|
|
|
content_types_accepted(Req, State) ->
|
|
case cowboy_req:method(Req) of
|
|
<<"PUT">> ->
|
|
{[{<<"application/json">>, create_user}], Req, State};
|
|
<<"POST">> ->
|
|
{[{<<"application/json">>, modify_user}], Req, State}
|
|
end.
|
|
|
|
resource_exists(Req, State) ->
|
|
{User, _} = dudeswave_auth:auth_cookies(Req),
|
|
|
|
case dudeswave_auth:details(User) of
|
|
[] ->
|
|
{false, Req, State};
|
|
{error, _} ->
|
|
{false, Req, State};
|
|
Details ->
|
|
NewState = State#{
|
|
details => Details,
|
|
user_exists => true,
|
|
request => cowboy_req:method(Req)
|
|
},
|
|
{true, Req, NewState}
|
|
end.
|
|
|
|
previously_existed(Req, State) -> {false, Req, State}.
|
|
|
|
is_conflict(Req, #{user_exists := true, request := <<"PUT">>}) ->
|
|
{true, Req, []};
|
|
|
|
is_conflict(Req, State) -> {false, Req, State}.
|
|
|
|
allow_missing_post(Req, State) -> {false, Req, State}.
|
|
|
|
delete_resource(Req, State) ->
|
|
{User, _} = dudeswave_auth:auth_cookies(Req),
|
|
|
|
case dudeswave_auth:delete(User) of
|
|
ok -> {true, Req, State};
|
|
{error, _} -> {false, Req, State}
|
|
end.
|
|
|
|
delete_completed(Req, State) -> {true, Req, State}.
|
|
|
|
%
|
|
% Custom callbacks
|
|
%
|
|
|
|
create_user(Req, State) ->
|
|
{User, Pass, Email, Req0} = dudeswave_auth:read_new_user_data(Req),
|
|
|
|
case dudeswave_auth:new(User, Pass, Email) of
|
|
ok -> {true, Req0, []};
|
|
{error, _} -> {false, Req0, State}
|
|
end.
|
|
|
|
modify_user(Req, State) ->
|
|
{User, _} = dudeswave_auth:auth_cookies(Req),
|
|
{Email, Desc, Name, Req0} = dudeswave_auth:read_update_user_data(Req),
|
|
|
|
case dudeswave_auth:update(User, Name, Email, Desc) of
|
|
ok -> {true, Req0, []};
|
|
{error, _} -> {false, Req0, State}
|
|
end.
|
|
|
|
user_details(Req, State) ->
|
|
{User, _} = dudeswave_auth:auth_cookies(Req),
|
|
#{details := Details} = State,
|
|
Data = Details#{user => User},
|
|
|
|
{iolist_to_binary(json:encode(Data)), Req, State}.
|
|
|
|
%
|
|
% gen_server callbacks
|
|
%
|
|
|
|
terminate(_Reason, _Req, _State) -> ok.
|