Skip to content

Commit

Permalink
feat: limited to the number of boards they can join
Browse files Browse the repository at this point in the history
  • Loading branch information
mrgleam committed Oct 8, 2024
1 parent d25585c commit 8d394ff
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 14 deletions.
89 changes: 88 additions & 1 deletion .github/workflows/seeds/boards.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,92 @@
INSERT INTO "public"."boards" ("id", "owner_id", "created_at", "updated_at", "deleted_at") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc07419e', '122e14c5-1e9f-40a7-b376-e3dce21ff85a', '2024-08-07 04:14:19.416824', '2024-08-07 04:14:19.416824', NULL);

INSERT INTO "public"."boards" ("id", "owner_id", "created_at", "updated_at", "deleted_at") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc07419f', '122e14c5-1e9f-40a7-b376-e3dce21ff85b', '2024-08-07 04:14:19.416824', '2024-08-07 04:14:19.416824', NULL);

INSERT INTO "public"."boards" ("id", "owner_id", "created_at", "updated_at", "deleted_at") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074110', '122e14c5-1e9f-40a7-b376-e3dce21ff85c', '2024-08-07 04:14:19.416824', '2024-08-07 04:14:19.416824', NULL);

INSERT INTO "public"."boards" ("id", "owner_id", "created_at", "updated_at", "deleted_at") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074111', '122e14c5-1e9f-40a7-b376-e3dce21ff85d', '2024-08-07 04:14:19.416824', '2024-08-07 04:14:19.416824', NULL);

INSERT INTO "public"."boards" ("id", "owner_id", "created_at", "updated_at", "deleted_at") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074112', '122e14c5-1e9f-40a7-b376-e3dce21ff85e', '2024-08-07 04:14:19.416824', '2024-08-07 04:14:19.416824', NULL);

INSERT INTO "public"."boards" ("id", "owner_id", "created_at", "updated_at", "deleted_at") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074113', '122e14c5-1e9f-40a7-b376-e3dce21ff85f', '2024-08-07 04:14:19.416824', '2024-08-07 04:14:19.416824', NULL);

INSERT INTO "public"."boards" ("id", "owner_id", "created_at", "updated_at", "deleted_at") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074114', '122e14c5-1e9f-40a7-b376-e3dce21ff860', '2024-08-07 04:14:19.416824', '2024-08-07 04:14:19.416824', NULL);

INSERT INTO "public"."boards" ("id", "owner_id", "created_at", "updated_at", "deleted_at") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074115', '122e14c5-1e9f-40a7-b376-e3dce21ff861', '2024-08-07 04:14:19.416824', '2024-08-07 04:14:19.416824', NULL);

INSERT INTO "public"."boards" ("id", "owner_id", "created_at", "updated_at", "deleted_at") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074116', '122e14c5-1e9f-40a7-b376-e3dce21ff862', '2024-08-07 04:14:19.416824', '2024-08-07 04:14:19.416824', NULL);

INSERT INTO "public"."boards" ("id", "owner_id", "created_at", "updated_at", "deleted_at") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074117', '122e14c5-1e9f-40a7-b376-e3dce21ff863', '2024-08-07 04:14:19.416824', '2024-08-07 04:14:19.416824', NULL);

INSERT INTO "public"."boards" ("id", "owner_id", "created_at", "updated_at", "deleted_at") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074118', '122e14c5-1e9f-40a7-b376-e3dce21ff864', '2024-08-07 04:14:19.416824', '2024-08-07 04:14:19.416824', NULL);

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc07419e', '122e14c5-1e9f-40a7-b376-e3dce21ff85a');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc07419f', '122e14c5-1e9f-40a7-b376-e3dce21ff85b');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074110', '122e14c5-1e9f-40a7-b376-e3dce21ff85c');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074111', '122e14c5-1e9f-40a7-b376-e3dce21ff85d');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074112', '122e14c5-1e9f-40a7-b376-e3dce21ff85e');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074113', '122e14c5-1e9f-40a7-b376-e3dce21ff85f');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074114', '122e14c5-1e9f-40a7-b376-e3dce21ff860');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074115', '122e14c5-1e9f-40a7-b376-e3dce21ff861');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074116', '122e14c5-1e9f-40a7-b376-e3dce21ff862');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074117', '122e14c5-1e9f-40a7-b376-e3dce21ff863');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074118', '122e14c5-1e9f-40a7-b376-e3dce21ff864');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc07419e', '122e14c5-1e9f-40a7-b376-e3dce21ff864');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc07419f', '122e14c5-1e9f-40a7-b376-e3dce21ff864');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074110', '122e14c5-1e9f-40a7-b376-e3dce21ff864');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074111', '122e14c5-1e9f-40a7-b376-e3dce21ff864');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074112', '122e14c5-1e9f-40a7-b376-e3dce21ff864');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074113', '122e14c5-1e9f-40a7-b376-e3dce21ff864');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074114', '122e14c5-1e9f-40a7-b376-e3dce21ff864');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc074115', '122e14c5-1e9f-40a7-b376-e3dce21ff864');

INSERT INTO "public"."boards_users" ("board_id", "user_id") VALUES
('1cde4b2c-5f94-4feb-9b11-cb23dc07419e', '122e14c5-1e9f-40a7-b376-e3dce21ff85a');
('1cde4b2c-5f94-4feb-9b11-cb23dc074116', '122e14c5-1e9f-40a7-b376-e3dce21ff864');
32 changes: 31 additions & 1 deletion .github/workflows/seeds/users.sql
Original file line number Diff line number Diff line change
@@ -1,2 +1,32 @@
INSERT INTO "public"."users" ("id", "email", "password", "is_verified", "created_at", "updated_at", "deleted_at") VALUES
('122e14c5-1e9f-40a7-b376-e3dce21ff85a', '[email protected]', '$2a$12$jR9mJLpurR6ej45plBgP2u7pZ.DMTaQDDgqtiFaHSiENtyoHnIKZK', 't', NULL, NULL, NULL);
('122e14c5-1e9f-40a7-b376-e3dce21ff85a', '[email protected]', '$2a$12$jR9mJLpurR6ej45plBgP2u7pZ.DMTaQDDgqtiFaHSiENtyoHnIKZK', 't', NULL, NULL, NULL);

INSERT INTO "public"."users" ("id", "email", "password", "is_verified", "created_at", "updated_at", "deleted_at") VALUES
('122e14c5-1e9f-40a7-b376-e3dce21ff85b', '[email protected]', '$2a$12$jR9mJLpurR6ej45plBgP2u7pZ.DMTaQDDgqtiFaHSiENtyoHnIKZK', 't', NULL, NULL, NULL);

INSERT INTO "public"."users" ("id", "email", "password", "is_verified", "created_at", "updated_at", "deleted_at") VALUES
('122e14c5-1e9f-40a7-b376-e3dce21ff85c', '[email protected]', '$2a$12$jR9mJLpurR6ej45plBgP2u7pZ.DMTaQDDgqtiFaHSiENtyoHnIKZK', 't', NULL, NULL, NULL);

INSERT INTO "public"."users" ("id", "email", "password", "is_verified", "created_at", "updated_at", "deleted_at") VALUES
('122e14c5-1e9f-40a7-b376-e3dce21ff85d', '[email protected]', '$2a$12$jR9mJLpurR6ej45plBgP2u7pZ.DMTaQDDgqtiFaHSiENtyoHnIKZK', 't', NULL, NULL, NULL);

INSERT INTO "public"."users" ("id", "email", "password", "is_verified", "created_at", "updated_at", "deleted_at") VALUES
('122e14c5-1e9f-40a7-b376-e3dce21ff85e', '[email protected]', '$2a$12$jR9mJLpurR6ej45plBgP2u7pZ.DMTaQDDgqtiFaHSiENtyoHnIKZK', 't', NULL, NULL, NULL);

INSERT INTO "public"."users" ("id", "email", "password", "is_verified", "created_at", "updated_at", "deleted_at") VALUES
('122e14c5-1e9f-40a7-b376-e3dce21ff85f', '[email protected]', '$2a$12$jR9mJLpurR6ej45plBgP2u7pZ.DMTaQDDgqtiFaHSiENtyoHnIKZK', 't', NULL, NULL, NULL);

INSERT INTO "public"."users" ("id", "email", "password", "is_verified", "created_at", "updated_at", "deleted_at") VALUES
('122e14c5-1e9f-40a7-b376-e3dce21ff860', '[email protected]', '$2a$12$jR9mJLpurR6ej45plBgP2u7pZ.DMTaQDDgqtiFaHSiENtyoHnIKZK', 't', NULL, NULL, NULL);

INSERT INTO "public"."users" ("id", "email", "password", "is_verified", "created_at", "updated_at", "deleted_at") VALUES
('122e14c5-1e9f-40a7-b376-e3dce21ff861', '[email protected]', '$2a$12$jR9mJLpurR6ej45plBgP2u7pZ.DMTaQDDgqtiFaHSiENtyoHnIKZK', 't', NULL, NULL, NULL);

INSERT INTO "public"."users" ("id", "email", "password", "is_verified", "created_at", "updated_at", "deleted_at") VALUES
('122e14c5-1e9f-40a7-b376-e3dce21ff862', '[email protected]', '$2a$12$jR9mJLpurR6ej45plBgP2u7pZ.DMTaQDDgqtiFaHSiENtyoHnIKZK', 't', NULL, NULL, NULL);

INSERT INTO "public"."users" ("id", "email", "password", "is_verified", "created_at", "updated_at", "deleted_at") VALUES
('122e14c5-1e9f-40a7-b376-e3dce21ff863', '[email protected]', '$2a$12$jR9mJLpurR6ej45plBgP2u7pZ.DMTaQDDgqtiFaHSiENtyoHnIKZK', 't', NULL, NULL, NULL);

INSERT INTO "public"."users" ("id", "email", "password", "is_verified", "created_at", "updated_at", "deleted_at") VALUES
('122e14c5-1e9f-40a7-b376-e3dce21ff864', '[email protected]', '$2a$12$jR9mJLpurR6ej45plBgP2u7pZ.DMTaQDDgqtiFaHSiENtyoHnIKZK', 't', NULL, NULL, NULL);
8 changes: 4 additions & 4 deletions e2e/tests/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { expect } from '@playwright/test';
import { test as setup } from './my-test';

const authFile = 'playwright/.auth/user.json';
const authFile01 = 'playwright/.auth/user01.json';

setup('authenticate', async ({ page, boardId }) => {
// Perform authentication steps. Replace these actions with your own.
await page.goto('/signin');
await page.getByLabel('Email address').fill('aaa@aaa.com');
await page.getByLabel('Email address').fill('aaa01@aaa.com');
await page.getByLabel('Password').fill('aaaaaaaa');
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait until the page receives the cookies.
Expand All @@ -19,5 +19,5 @@ setup('authenticate', async ({ page, boardId }) => {

// End of authentication steps.

await page.context().storageState({ path: authFile });
});
await page.context().storageState({ path: authFile01 });
});
52 changes: 52 additions & 0 deletions e2e/tests/example.spec.ts → e2e/tests/board.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,55 @@ test.describe('Item', () => {
await expect(page.getByTestId("done-items")).toContainText([TODO_ITEMS[0]]);
})
});

test.describe('Validate', () => {
test('should allow me to add user', async ({ page, context }) => {
await page.getByTestId("show-menu").click();

const [newPage] = await Promise.all([
context.waitForEvent('page'), // get `context` by destructuring with `page` in the test params; 'page' is a built-in event, and **you must wait for this like this,**, or `newPage` will just be the response object, rather than an actual Playwright page object.
await page.getByTestId("invite").click() // note that, like all waiting in Playwright, this is somewhat unintuitive. This is the action which is *causing the navigation*; you have to set up the wait *before* it happens, hence the use of Promise.all().
]);

await newPage.waitForLoadState(); // wait for the new tab to fully load
// now, use `newPage` to access the newly opened tab, rather than `page`, which will still refer to the original page/tab.
await expect(newPage).toHaveURL(/invite/);

const email = await newPage.getByPlaceholder('Please enter email');

email.fill('[email protected]');

const responseSubmit = newPage.waitForResponse('**/invite');

await newPage.getByTestId("submit").click();

const response = await responseSubmit;
expect(response.status()).toBe(200);
expect(newPage.getByText('Invitation Successful!')).toBeVisible();

});

test('should not allow me to add user when the user is limited to the number of boards they can join', async ({ page, context }) => {
await page.getByTestId("show-menu").click();

const [newPage] = await Promise.all([
context.waitForEvent('page'), // get `context` by destructuring with `page` in the test params; 'page' is a built-in event, and **you must wait for this like this,**, or `newPage` will just be the response object, rather than an actual Playwright page object.
await page.getByTestId("invite").click() // note that, like all waiting in Playwright, this is somewhat unintuitive. This is the action which is *causing the navigation*; you have to set up the wait *before* it happens, hence the use of Promise.all().
]);

await newPage.waitForLoadState(); // wait for the new tab to fully load
// now, use `newPage` to access the newly opened tab, rather than `page`, which will still refer to the original page/tab.
await expect(newPage).toHaveURL(/invite/);

const email = await newPage.getByPlaceholder('Please enter email');

email.fill('[email protected]');

const responseSubmit = newPage.waitForResponse('**/invite');

await newPage.getByTestId("submit").click();

const response = await responseSubmit;
expect(response.status()).toBe(403);
});
});
15 changes: 13 additions & 2 deletions src/app/components/nav.gleam
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import gleam/int
import gleam/list
import gleam/string
import lustre/attribute
import lustre/element
import lustre/element/html
Expand Down Expand Up @@ -31,7 +32,13 @@ fn nav_header() -> element.Element(t) {
html.i([attribute.class("fa-solid fa-person-chalkboard")], []),
]),
html.label([attribute.for("nav-toggle")], [
html.span([attribute.id("nav-toggle-burger")], []),
html.span(
[
attribute.id("nav-toggle-burger"),
attribute.attribute("data-testid", "show-menu"),
],
[],
),
]),
html.hr([]),
])
Expand Down Expand Up @@ -99,7 +106,11 @@ fn nav_button(

html.div([attribute.class("nav-button")], [
html.i([attribute.class(icon_class)], []),
html.form(form_attributes, [html.button([], [element.text(text)])]),
html.form(form_attributes, [
html.button([attribute.attribute("data-testid", string.lowercase(text))], [
element.text(text),
]),
]),
])
}

Expand Down
33 changes: 33 additions & 0 deletions src/app/models/board_user.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,25 @@ pub fn list_board_user(user_id: String, db: Connection) -> List(Board) {
returned.rows
}

pub fn count_board_user(user_id: String, db: Connection) -> Int {
let sql =
"
SELECT
count(*)
FROM
boards_users
WHERE
user_id = $1
"

let assert Ok(returned) =
pgo.execute(sql, db, [pgo.text(user_id)], dynamic.element(0, dynamic.int))

let assert [count] = returned.rows

count
}

pub fn join(req_token: String, db: Connection, redis: Subject(Message)) {
use data <- result.try(
radish.get(redis, req_token, constant.timeout)
Expand All @@ -93,3 +112,17 @@ pub fn join(req_token: String, db: Connection, redis: Subject(Message)) {

create_board_user(data.board_id, data.user_id, db)
}

pub fn validate_board_user(
user_id: String,
db: Connection,
) -> Result(Int, AppError) {
case count_board_user(user_id, db) {
a if a >= 10 -> {
Error(error.BadRequest)
}
a -> {
Ok(a)
}
}
}
2 changes: 2 additions & 0 deletions src/app/pages/invite.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ pub fn root(board_id: String, error: String) -> Element(t) {
),
attribute.id("email"),
attribute.name("email"),
attribute.placeholder("Please enter email"),
attribute.type_("email"),
]),
]),
Expand All @@ -64,6 +65,7 @@ pub fn root(board_id: String, error: String) -> Element(t) {
html.button(
[
attribute.type_("sumbit"),
attribute.attribute("data-testid", "submit"),
attribute.class(
"flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600",
),
Expand Down
23 changes: 17 additions & 6 deletions src/app/routes/user_routes.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import app/error
import app/helpers/constant
import app/helpers/uuid
import app/models/board.{create_board}
import app/models/board_user.{create_board_user, list_board_user}
import app/models/board_user.{
create_board_user, list_board_user, validate_board_user,
}
import app/models/email.{send_forgot_password, send_invite, send_verify_user}
import app/models/user.{
create_user, get_user_by_email, signin_user, update_password_user,
Expand Down Expand Up @@ -342,7 +344,7 @@ pub fn post_invite(req: Request, ctx: Context) {

let email_validator = valid.string_is_email("Not email")

let _result = {
let result = {
use user_email <- result.try(
list.key_find(form.values, "email")
|> result.map_error(fn(_) { error.BadRequest }),
Expand All @@ -369,6 +371,8 @@ pub fn post_invite(req: Request, ctx: Context) {
|> result.flatten,
)

use _ <- result.try(validate_board_user(user_id, ctx.db))

let token = minigen.string(20) |> minigen.run

use _ <- result.try(
Expand All @@ -394,10 +398,17 @@ pub fn post_invite(req: Request, ctx: Context) {
send_invite(ctx.email_api_key, user_email, invite_link)
}

[pages.submit_invite()]
|> layout
|> element.to_document_string_builder
|> wisp.html_response(200)
case result {
Ok(_) -> {
[pages.submit_invite()]
|> layout
|> element.to_document_string_builder
|> wisp.html_response(200)
}
Error(_) -> {
wisp.response(403)
}
}
}

pub fn join_board(req: Request, ctx: Context) {
Expand Down

0 comments on commit 8d394ff

Please sign in to comment.