This is the backend side of the ABS App made with Go (Fiber, GORM PostgreSQL) for ordering and managing at ABS.
This project uses the GNU GPL v2.0 license.
I developed this app on Ubuntu 22.04 in WSL2, so all the commands you will see will be Unix commands. It will be slightly different on Windows.
Clone this repo and then cd
into it. To develop this app locally first you must
have go installed on your machine. If you already have Go, install all dependencies
with the command go get .
.
Then copy the .env.example
file and name the new file to .env
, and fill all
the fields with the appropriate data.
After all setup is done, you can go ahead and run go run .
, and Fiber will tell
you that the server is running on localhost:8080
.
This project uses PostgreSQL. Install it locally on your machine, database migrations
will run automatically when you run the command go run .
. For now, the seeder only
has the user roles and one admin user.
The database dump is now available (abs.sql
).
In order of priority.
Implement auth- implement refresh token
implement orderingfor anonymous user (e.g. most people)for authenticated usersimplement order completion- omit password field in orders
implement limiting- implement better error handling for various edge cases
signup- login
make.sql
file for database migrations- make documentation (in progress)
implement pagination and limiting- add unit testing
- migrate code to use Docker (lmao I will probably not do this)
- final configurations and deploy!!
- make a frontend application probably
For the end user/application, are two main endpoints on each you can do CRUD operations:
/api/menu
/api/orders
Each item in /menu
also has /variant-values
which contains each menu's variants
and respective prices.
There are also three endpoints for the admin:
/admin/menu
/admin/users
/admin/orders
All requests have a data (object or array) and error (bool) property. The err
field will be true if there are any errors, client-side or server-side. In that case,
a field named message
will tell you exactly what happened.
{
"err": true,
"message": "..."
}
For example, if you tried to access a protected endpoint (say /api/orders
)
without valid credentials, you would get the following response:
{
"err": true,
"message": "cannot verify token"
}
I will make an exhaustive list of error messages soon.
To sign up a user, issue a POST request to /signup
with a JSON body with the
following fields:
name
(string)email
(string): has to be valid emailpassword
(string): has to be >= 8 characters long
{
"name": "Muhammad Rava",
"email": "rava@gmail.com",
"password": "secret-password"
}
A successful response looks like this:
{
"err": false,
"message": "signup success! redirect user to login page"
}
You will get the following error if email is not valid or password is less then 8 characters long:
{
"err": true,
"message": "email has to be valid, and password has to be at least 8 characters long"
}
To log a user in, issue a POST request to /signin
with a JSON body with these
fields:
email
(string)password
(string)
{
"email": "rava@gmail.com",
"password": "secret-password"
}
A successful response looks like:
{
"err": false,
"token": "jwttoken",
"message": "successfully logged in"
}
The server will issue a token in the response for the client to log in, and also set a cookie with the token which can be used for web apps.
Every request to a protected endpoint has to have the token issued by the server
either in a cookie or in an Authorization
token.
To log a user out, issue a POST request to /logout
with no body. A successful
request yields a response like this:
{
"err": false,
"message": "successfully logged out"
}
To get all menu items, you can issue a GET request to /api/menu
. You can also
search by name or filter by type by adding an URL parameter like so:
/api/menu?name=searchterm&type_id=10
.
A successful response looks like this:
successful response example
{
"data": [
{
"id": "b983ff25-c532-43ea-aee7-fc75eaa7c2bb",
"name": "Espresso",
"type_id": 1,
"type": {
"id": 1,
"type": "kopi"
},
"created_at": "2024-01-19T08:56:35.169389+07:00",
"updated_at": "2024-01-19T08:56:35.169389+07:00",
"variant_values": [
{
"menu_id": "b983ff25-c532-43ea-aee7-fc75eaa7c2bb",
"option_id": 1,
"option_value_id": 2,
"option": {
"id": 1,
"name": "temp"
},
"option_value": {
"id": 2,
"option_id": 1,
"value": "hot"
},
"price": 6000
},
{
"menu_id": "b983ff25-c532-43ea-aee7-fc75eaa7c2bb",
"option_id": 1,
"option_value_id": 1,
"option": {
"id": 1,
"name": "temp"
},
"option_value": {
"id": 1,
"option_id": 1,
"value": "iced"
},
"price": 10000
}
]
}
],
"error": false
}
You can also limit and paginate the response by adding limit and page query to your request.
Page defaults to 1 if not specified. If supplied page is or less than 0, it will default to 1.
The maximum limit is 100, if supplied limit is 0 or more than 100, it defaults to 100. For example:
localhost:8080/api/menu?limit=100
-> gets 1st page with 100 items in the page
localhost:8080/api/menu?page=10
-> gets 10th page with 20 items in the page
localhost:8080/api/menu?page=4?limit=10
-> gets 4th page with 10 items in the page
You can also get a menu item by ID by issuing a GET request to /api/menu/valid-menu-id
Where valid-menu-id
is a valid menu UUID. A successful response looks like this:
Successful response example
{
"data": {
"id": "b983ff25-c532-43ea-aee7-fc75eaa7c2bb",
"name": "Espresso",
"type_id": 1,
"type": {
"id": 1,
"type": "kopi"
},
"created_at": "2024-01-19T08:56:35.169389+07:00",
"updated_at": "2024-01-19T08:56:35.169389+07:00",
"variant_values": [
{
"menu_id": "b983ff25-c532-43ea-aee7-fc75eaa7c2bb",
"option_id": 1,
"option_value_id": 2,
"option": {
"id": 1,
"name": "temp"
},
"option_value": {
"id": 2,
"option_id": 1,
"value": "hot"
},
"price": 6000
},
{
"menu_id": "b983ff25-c532-43ea-aee7-fc75eaa7c2bb",
"option_id": 1,
"option_value_id": 1,
"option": {
"id": 1,
"name": "temp"
},
"option_value": {
"id": 1,
"option_id": 1,
"value": "iced"
},
"price": 10000
}
]
},
"err": false
}
To get orders you can issue a GET request to /api/orders
; this gets all
complete orders for the current users by default. You can filter by status
by adding the query parameter is_completed
to the URL, for example to get incomplete
orders:
/api/orders?is_completed=false
If successful, you will get a response following this structure:
Successful response example
{
"data": [
{
"id": "686156e2-993b-4518-ab42-e757335fcd75",
"user_id": "fad6c002-cbba-48dc-81d8-d56a17f5428c",
"user": {
"id": "fad6c002-cbba-48dc-81d8-d56a17f5428c",
"name": "Abiman You",
"email": "abim@abim.com",
"password": "$2a$14$DhwQKFBX8ZVoIJqBokT6guiKeJ063uBpekz.ZM1ISncnhe.xm1/Qe",
"role_id": 1,
"role": {
"id": 1,
"name": "user"
},
"created_at": "2024-02-19T10:01:05.918176+07:00",
"updated_at": "2024-02-19T10:01:05.918176+07:00"
},
"created_at": "2024-02-20T15:35:39.374745+07:00",
"is_completed": false,
"completed_at": "0001-01-01T00:00:00Z",
"order_details": [
{
"order_id": "686156e2-993b-4518-ab42-e757335fcd75",
"menu_id": "ab2a528c-9c5b-45d0-ba7f-ce91a97c6b67",
"menu_name": "Banana Strawberry",
"menu_type": "pizza",
"menu_option_id": 35,
"menu_option": "pizza topping",
"menu_option_value_id": 36,
"menu_option_value": "regular",
"quantity": 2,
"total_price": 90000
}
]
}
],
"err": false
}
You can also get individual orders by issuing a GET request to /api/orders/:id
where id
is a valid order ID.
This endpoint is protected by auth. Makes sure you have logged in before making a request. To place a new order, issue a POST request with the following body:
{
"order_details": [
{
"menu_id": "valid menu ID here",
"menu_option_id": 999,
"menu_option_value_id": 999,
"quantity": 1
}
]
}
(UNSTABLE: anonymous ordering) If placing a new order for an anonymous user, you can supply a username:
{
"username": "Rava Basya",
"order_details": [
{
"menu_id": "valid menu ID here",
"menu_option_id": 999,
"menu_option_value_id": 999,
"quantity": 1
}
]
}
The order_details
field is an array containing however many items you want to
order. The item is an object with the following fields:
menu_id
(uuid): ID of menu itemmenu_option_id
(int): ID of menu optionmenu_option_value_id
(int): Id of menu option value
You need to specify menu_option_id
and menu_option_value_id
according to the
menu type. For example, the pizza menu type has 35
for option_id
(pizza topping)
and 36
(regular) for one of the option_value_id
, therefore your order should
look like this:
{
"order_details": [
{
"menu_id": "bf5aad5a-4d81-4ab2-ae64-d0dad9b77061",
"menu_option_id": 35,
"menu_option_value_id": 36,
"quantity": 1
}
]
}
You can look at the available options and the corresponding option values when getting menu information.
All admin endpoints are located in /admin
. Here you can administer orders, users
and menu items as admin, but to do those things you need to be authenticated.
Head over to /signin
and sign the admin in.
to create a new menu item, issue a POST request to /admin/menu
with the following
body:
{
"name": "a new delicious drink",
"type_id": 6,
"variant_values": [
{
"option_id": 1,
"option_value_id": 37,
"price": 8000
},
{
"option_id": 1,
"option_value_id": 2,
"price": 10000
}
]
}
TO update an existing menu item, issue a POST request to /admin/menu
with the following
body:
{
"name": "Minuman ngetes doang",
"variant_values": [
{
"option_id": 1,
"option_value_id": 2,
"price": 10000
}
]
}
The server will only update non nil values. In the example above, the server will
update name and the price of menu item where option_id
is 1 and option_value_id
is 2.
To delete a menu item, issue a DELETE request to /admin/menu/:id
where id
is a valid menu item ID of type UUID you want to delete. A successful response looks
like this:
{
"err": false,
"message": "menu item successfully deleted"
}
You can also batch delete (make a DELETE request to /admin/menu/
) by supplying
the UUIDs of menu items in an array like so:
["menu-id-1", "menu-id-2"]
To insert a new menu price of a menu item, issue a POST request to /admin/menu/:id/variant_values
where id
is the menu item ID of type UUID. You can attach a body to the request
like so:
[
{
"option_id": 999,
"option_value_id": 999,
"price": 16000
}
]
You can include all the prices needed in the array.
If the option_id and/or option_value_id does not exist, you will get the following response:
Error response
{
"err": true,
"message": "make sure both option_id and option_value_id are valid"
}
To edit a specific price of a menu item, issue a PATCH /admin/menu/:id/variant_values
where id
is a valid menu item ID of type UUID. Attach a body specifying which
combination of option_id and option_value_id you want to edit, and the new price:
[
{
"option_id": 1,
"option_value_id": 1,
"price": 15000
}
]
You can put as many combination as needed in the array.
To delete a price of a menu item, issue a DELETE request to /admin/menu/:id/variant_values
where id
is a valid menu item ID of type UUID with the following body:
{
"option_id": 1,
"option_value_id": 37
}
To get all orders of all users, issue a GET request to /admin/orders/
. You can
also get orders by ID (/admin/orders/:id
) where id is a valid order ID.
Successful response example
{
"data": [
{
"id": "26a33223-da03-4a5c-8bbb-f7fd6944abef",
"user_id": "46e11084-0baa-4d2f-bf52-6f6a93a78619",
"user": {
"id": "46e11084-0baa-4d2f-bf52-6f6a93a78619",
"name": "Muhammad Rava",
"email": "rava@gmail.com",
"password": "password",
"role_id": 1,
"role": {
"id": 1,
"name": "user"
},
"created_at": "2024-02-21T10:12:43.369861+07:00",
"updated_at": "2024-02-21T10:12:43.369861+07:00"
},
"created_at": "2024-02-21T10:18:19.674673+07:00",
"is_completed": true,
"completed_at": "2024-02-21T10:26:54.973271+07:00",
"order_details": [
{
"order_id": "26a33223-da03-4a5c-8bbb-f7fd6944abef",
"menu_id": "ab2a528c-9c5b-45d0-ba7f-ce91a97c6b67",
"menu_name": "Banana Strawberry",
"menu_type": "pizza",
"menu_option_id": 35,
"menu_option": "pizza topping",
"menu_option_value_id": 36,
"menu_option_value": "regular",
"quantity": 2,
"total_price": 90000
}
]
}
],
"err": false
}
error response
{
"err": true,
"message": "error when querying database"
}
To complete an order, issue a PATCH request (/admin/orders/:id
) where id
is
a valid order ID. Note that you don't have to attach a body to the request.
Successful response example
{
"err": false,
"message": "order of ID <id> succesfully completed"
}
To get all users, issue a GET request to /admin/users
.
Succcesful response example
{
"data": [
{
"id": "fad6c002-cbba-48dc-81d8-d56a17f5428c",
"name": "Abiman You",
"email": "abim@abim.com",
"password": "",
"role_id": 1,
"role": {
"id": 1,
"name": "user"
},
"created_at": "2024-02-19T10:01:05.918176+07:00",
"updated_at": "2024-02-19T10:01:05.918176+07:00"
}
],
"err": false
}
You can also get a specific user by adding an id (/admin/users/:id
).
Successful response example
{
"data": {
"id": "fad6c002-cbba-48dc-81d8-d56a17f5428c",
"name": "Abiman You",
"email": "abim@abim.com",
"password": "",
"role_id": 1,
"role": {
"id": 1,
"name": "user"
},
"created_at": "2024-02-19T10:01:05.918176+07:00",
"updated_at": "2024-02-19T10:01:05.918176+07:00"
},
"err": false
}