Wip backend #4

Merged
AndrewTrieu merged 6 commits from wip-backend into main 2024-11-23 16:17:00 +00:00
39 changed files with 5438 additions and 100 deletions

10
.gitignore vendored
View File

@@ -22,7 +22,13 @@
**/next-env.d.ts **/next-env.d.ts
# images # images
**/public/** tasker-client/public/**
# terraform # terraform
**/**/.terraform** **/.terraform**
# serverless
**/.serverless/**
# zipped layers
**/layers/**.zip

View File

@@ -23,7 +23,14 @@ const ModalNewTask = ({ isOpen, onClose, id = null }: Props) => {
const [projectId, setProjectId] = useState(""); const [projectId, setProjectId] = useState("");
const handleSubmit = async () => { const handleSubmit = async () => {
if (!title || !authorUserId || !(id !== null || projectId)) return; console.log(title, authorUserId, id, projectId);
console.log("Creating task 1..");
if (
!(title && authorUserId && assignedUserId && (id !== null || projectId))
)
return;
console.log("Creating task 2...");
const formattedStartDate = formatISO(new Date(startDate), { const formattedStartDate = formatISO(new Date(startDate), {
representation: "complete", representation: "complete",
@@ -40,14 +47,17 @@ const ModalNewTask = ({ isOpen, onClose, id = null }: Props) => {
tags, tags,
startDate: formattedStartDate, startDate: formattedStartDate,
dueDate: formattedDueDate, dueDate: formattedDueDate,
authorUserId: parseInt(authorUserId), authorUserId: authorUserId,
assignedUserId: parseInt(assignedUserId), assignedUserId: assignedUserId,
projectId: id !== null ? Number(id) : Number(projectId), projectId: id !== null ? id : projectId,
}); });
}; };
const isFormValid = () => { const isFormValid = () => {
return title && authorUserId && !(id !== null || projectId); console.log(title, authorUserId, id, projectId);
return (
title && authorUserId && assignedUserId && (id !== null || projectId)
);
}; };
const selectStyles = const selectStyles =
@@ -87,7 +97,7 @@ const ModalNewTask = ({ isOpen, onClose, id = null }: Props) => {
} }
> >
<option value="">Select Status</option> <option value="">Select Status</option>
<option value={Status.ToDo}>Backlog</option> <option value={Status.ToDo}>To Do</option>
<option value={Status.InProgress}>In Progress</option> <option value={Status.InProgress}>In Progress</option>
<option value={Status.TestReview}>Test/Review</option> <option value={Status.TestReview}>Test/Review</option>
<option value={Status.Done}>Done</option> <option value={Status.Done}>Done</option>

View File

@@ -77,7 +77,7 @@ const Navbar = () => {
<div className="align-center flex h-9 w-9 justify-center"> <div className="align-center flex h-9 w-9 justify-center">
{!!currentUserDetails?.profilePictureUrl ? ( {!!currentUserDetails?.profilePictureUrl ? (
<Image <Image
src={`/${currentUserDetails?.profilePictureUrl}`} src={`${process.env.S3_PUBLIC_IMAGE_URL}/${currentUserDetails?.profilePictureUrl}`}
alt={currentUserDetails?.username || "User Profile Picture"} alt={currentUserDetails?.username || "User Profile Picture"}
width={100} width={100}
height={50} height={50}

View File

@@ -70,7 +70,12 @@ const Sidebar = () => {
)} )}
</div> </div>
<div className="flex items-center gap-5 border-y-[1.5px] border-gray-200 px-8 py-4 dark:border-gray-700"> <div className="flex items-center gap-5 border-y-[1.5px] border-gray-200 px-8 py-4 dark:border-gray-700">
<Image src="/logo.png" alt="logo" width={40} height={40} /> <Image
src={`${process.env.S3_PUBLIC_IMAGE_URL}/logo.png`}
alt="logo"
width={40}
height={40}
/>
<div> <div>
<h3 className="text-md font-bold tracking-widest dark:text-gray-200"> <h3 className="text-md font-bold tracking-widest dark:text-gray-200">
Tasker.IO Tasker.IO
@@ -93,7 +98,6 @@ const Sidebar = () => {
<button <button
onClick={() => onClick={() =>
setShowProjects((prev) => { setShowProjects((prev) => {
console.log(prev);
return !prev; return !prev;
}) })
} }
@@ -109,10 +113,10 @@ const Sidebar = () => {
{showProjects && {showProjects &&
projects?.map((project) => ( projects?.map((project) => (
<SidebarLink <SidebarLink
key={project.id} key={project.projectId}
icon={Briefcase} icon={Briefcase}
label={project.name} label={project.name}
href={`/projects/${project.id}`} href={`/projects/${project.projectId}`}
/> />
))} ))}
@@ -158,7 +162,7 @@ const Sidebar = () => {
<div className="align-center flex h-9 w-9 justify-center"> <div className="align-center flex h-9 w-9 justify-center">
{!!currentUserDetails?.profilePictureUrl ? ( {!!currentUserDetails?.profilePictureUrl ? (
<Image <Image
src={`/${currentUserDetails?.profilePictureUrl}`} src={`${process.env.S3_PUBLIC_IMAGE_URL}/${currentUserDetails?.profilePictureUrl}`}
alt={currentUserDetails?.username || "User Profile Picture"} alt={currentUserDetails?.username || "User Profile Picture"}
width={100} width={100}
height={50} height={50}

View File

@@ -16,7 +16,7 @@ const TaskCard = ({ task }: Props) => {
<div className="flex flex-wrap"> <div className="flex flex-wrap">
{task.attachments && task.attachments.length > 0 && ( {task.attachments && task.attachments.length > 0 && (
<Image <Image
src={`/${task.attachments[0].fileURL}`} src={`${process.env.S3_PUBLIC_IMAGE_URL}/${task.attachments[0].fileURL}`}
alt={task.attachments[0].fileName} alt={task.attachments[0].fileName}
width={400} width={400}
height={200} height={200}
@@ -27,7 +27,7 @@ const TaskCard = ({ task }: Props) => {
</div> </div>
)} )}
<p> <p>
<strong>ID:</strong> {task.id} <strong>ID:</strong> {task.taskId}
</p> </p>
<p> <p>
<strong>Title:</strong> {task.title} <strong>Title:</strong> {task.title}

View File

@@ -11,7 +11,7 @@ const UserCard = ({ user }: Props) => {
<div className="flex items-center rounded border p-4 shadow"> <div className="flex items-center rounded border p-4 shadow">
{user.profilePictureUrl && ( {user.profilePictureUrl && (
<Image <Image
src={`/${user.profilePictureUrl}`} src={`${process.env.S3_PUBLIC_IMAGE_URL}/${user.profilePictureUrl}`}
alt="profile picture" alt="profile picture"
width={32} width={32}
height={32} height={32}

View File

@@ -5,6 +5,7 @@ import {
Priority, Priority,
Project, Project,
Task, Task,
useGetAuthUserQuery,
useGetProjectsQuery, useGetProjectsQuery,
useGetTasksByUserQuery, useGetTasksByUserQuery,
} from "@/state/api"; } from "@/state/api";
@@ -77,12 +78,13 @@ const taskColumns: GridColDef[] = [
const statusColors = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042"]; const statusColors = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042"];
const HomePage = () => { const HomePage = () => {
const userId = 1; const { data: currentUser } = useGetAuthUserQuery({});
const userId = currentUser?.userDetails?.userId ?? null;
const { const {
data: tasks, data: tasks,
isLoading: tasksLoading, isLoading: tasksLoading,
isError: tasksError, isError: tasksError,
} = useGetTasksByUserQuery(userId || 0, { } = useGetTasksByUserQuery(userId || "", {
skip: userId === null, skip: userId === null,
}); });
const { data: projects, isLoading: isProjectsLoading } = const { data: projects, isLoading: isProjectsLoading } =
@@ -191,6 +193,7 @@ const HomePage = () => {
columns={taskColumns} columns={taskColumns}
checkboxSelection checkboxSelection
loading={tasksLoading} loading={tasksLoading}
getRowId={(row) => row.taskId}
getRowClassName={() => "data-grid-row"} getRowClassName={() => "data-grid-row"}
getCellClassName={() => "data-grid-cell"} getCellClassName={() => "data-grid-cell"}
className="border border-gray-200 bg-white shadow dark:border-stroke-dark dark:bg-dark-secondary dark:text-gray-200" className="border border-gray-200 bg-white shadow dark:border-stroke-dark dark:bg-dark-secondary dark:text-gray-200"

View File

@@ -83,7 +83,7 @@ const ReusablePriorityPage = ({ priority }: Props) => {
data: tasks, data: tasks,
isLoading, isLoading,
isError: isTasksError, isError: isTasksError,
} = useGetTasksByUserQuery(userId || 0, { } = useGetTasksByUserQuery(userId || "", {
skip: userId === null, skip: userId === null,
}); });
@@ -135,7 +135,7 @@ const ReusablePriorityPage = ({ priority }: Props) => {
) : view === "list" ? ( ) : view === "list" ? (
<div className="grid grid-cols-1 gap-4"> <div className="grid grid-cols-1 gap-4">
{filteredTasks?.map((task: Task) => ( {filteredTasks?.map((task: Task) => (
<TaskCard key={task.id} task={task} /> <TaskCard key={task.taskId} task={task} />
))} ))}
</div> </div>
) : ( ) : (
@@ -146,7 +146,7 @@ const ReusablePriorityPage = ({ priority }: Props) => {
rows={filteredTasks} rows={filteredTasks}
columns={columns} columns={columns}
checkboxSelection checkboxSelection
getRowId={(row) => row.id} getRowId={(row) => row.taskId}
className="border border-gray-200 bg-white shadow dark:border-stroke-dark dark:bg-dark-secondary dark:text-gray-200" className="border border-gray-200 bg-white shadow dark:border-stroke-dark dark:bg-dark-secondary dark:text-gray-200"
sx={dataGridSxStyles(isDarkMode)} sx={dataGridSxStyles(isDarkMode)}
/> />

View File

@@ -1,6 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { useGetTasksQuery, useUpdateTaskStatusMutation } from "@/state/api"; import {
import React from "react"; Status,
useGetTasksQuery,
useUpdateTaskStatusMutation,
} from "@/state/api";
import React, { useEffect, useState } from "react";
import { DndProvider, useDrag, useDrop } from "react-dnd"; import { DndProvider, useDrag, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend"; import { HTML5Backend } from "react-dnd-html5-backend";
import { Task as TaskType } from "@/state/api"; import { Task as TaskType } from "@/state/api";
@@ -9,22 +13,40 @@ import { format } from "date-fns";
import Image from "next/image"; import Image from "next/image";
type BoardProps = { type BoardProps = {
id: string; projectId: string;
setIsModalNewTaskOpen: (isOpen: boolean) => void; setIsModalNewTaskOpen: (isOpen: boolean) => void;
}; };
const taskStatus = ["To Do", "In Progress", "Test/Review", "Done"]; const taskStatus = ["To Do", "In Progress", "Test/Review", "Done"];
const BoardView = ({ id, setIsModalNewTaskOpen }: BoardProps) => { const BoardView = ({ projectId, setIsModalNewTaskOpen }: BoardProps) => {
const { const {
data: tasks, data: fetchedTasks,
isLoading, isLoading,
error, error,
} = useGetTasksQuery({ projectId: Number(id) }); } = useGetTasksQuery({ projectId });
const [updateTaskStatus] = useUpdateTaskStatusMutation(); const [updateTaskStatus] = useUpdateTaskStatusMutation();
const [tasks, setTasks] = useState<TaskType[]>([]);
const moveTask = (taskId: number, toStatus: string) => { useEffect(() => {
updateTaskStatus({ taskId, status: toStatus }); if (fetchedTasks) {
setTasks(fetchedTasks);
}
}, [fetchedTasks]);
const moveTask = async (taskId: string, toStatus: string) => {
setTasks((prevTasks) =>
prevTasks.map((task) =>
task.taskId === taskId ? { ...task, status: toStatus as Status } : task,
),
);
try {
await updateTaskStatus({ taskId, status: toStatus });
} catch (error) {
console.error("Failed to update task status:", error);
setTasks(fetchedTasks || []);
}
}; };
if (isLoading) return <div>Loading...</div>; if (isLoading) return <div>Loading...</div>;
@@ -50,7 +72,7 @@ const BoardView = ({ id, setIsModalNewTaskOpen }: BoardProps) => {
type TaskColumnProps = { type TaskColumnProps = {
status: string; status: string;
tasks: TaskType[]; tasks: TaskType[];
moveTask: (taskId: number, toStatus: string) => void; moveTask: (taskId: string, toStatus: string) => void;
setIsModalNewTaskOpen: (isOpen: boolean) => void; setIsModalNewTaskOpen: (isOpen: boolean) => void;
}; };
@@ -62,7 +84,7 @@ const TaskColumn = ({
}: TaskColumnProps) => { }: TaskColumnProps) => {
const [{ isOver }, drop] = useDrop(() => ({ const [{ isOver }, drop] = useDrop(() => ({
accept: "task", accept: "task",
drop: (item: { id: number }) => moveTask(item.id, status), drop: (item: { id: string }) => moveTask(item.id, status),
collect: (monitor: any) => ({ collect: (monitor: any) => ({
isOver: !!monitor.isOver(), isOver: !!monitor.isOver(),
}), }),
@@ -116,7 +138,7 @@ const TaskColumn = ({
{tasks {tasks
.filter((task) => task.status === status) .filter((task) => task.status === status)
.map((task) => ( .map((task) => (
<Task key={task.id} task={task} /> <Task key={task.taskId} task={task} />
))} ))}
</div> </div>
); );
@@ -129,7 +151,7 @@ type TaskProps = {
const Task = ({ task }: TaskProps) => { const Task = ({ task }: TaskProps) => {
const [{ isDragging }, drag] = useDrag(() => ({ const [{ isDragging }, drag] = useDrag(() => ({
type: "task", type: "task",
item: { id: task.id }, item: { id: task.taskId },
collect: (monitor: any) => ({ collect: (monitor: any) => ({
isDragging: !!monitor.isDragging(), isDragging: !!monitor.isDragging(),
}), }),
@@ -175,7 +197,7 @@ const Task = ({ task }: TaskProps) => {
> >
{task.attachments && task.attachments.length > 0 && ( {task.attachments && task.attachments.length > 0 && (
<Image <Image
src={`/${task.attachments[0].fileURL}`} src={`${process.env.S3_PUBLIC_IMAGE_URL}/${task.attachments[0].fileURL}`}
alt={task.attachments[0].fileName} alt={task.attachments[0].fileName}
width={400} width={400}
height={200} height={200}
@@ -227,7 +249,7 @@ const Task = ({ task }: TaskProps) => {
{task.assignee && ( {task.assignee && (
<Image <Image
key={task.assignee.userId} key={task.assignee.userId}
src={`/${task.assignee.profilePictureUrl!}`} src={`${process.env.S3_PUBLIC_IMAGE_URL}/${task.assignee.profilePictureUrl!}`}
alt={task.assignee.username} alt={task.assignee.username}
width={30} width={30}
height={30} height={30}
@@ -237,7 +259,7 @@ const Task = ({ task }: TaskProps) => {
{task.author && ( {task.author && (
<Image <Image
key={task.author.userId} key={task.author.userId}
src={`/${task.author.profilePictureUrl!}`} src={`${process.env.S3_PUBLIC_IMAGE_URL}/${task.author.profilePictureUrl!}`}
alt={task.author.username} alt={task.author.username}
width={30} width={30}
height={30} height={30}

View File

@@ -4,16 +4,12 @@ import { Task, useGetTasksQuery } from "@/state/api";
import React from "react"; import React from "react";
type Props = { type Props = {
id: string; projectId: string;
setIsModalNewTaskOpen: (isOpen: boolean) => void; setIsModalNewTaskOpen: (isOpen: boolean) => void;
}; };
const ListView = ({ id, setIsModalNewTaskOpen }: Props) => { const ListView = ({ projectId, setIsModalNewTaskOpen }: Props) => {
const { const { data: tasks, error, isLoading } = useGetTasksQuery({ projectId });
data: tasks,
error,
isLoading,
} = useGetTasksQuery({ projectId: Number(id) });
if (isLoading) return <div>Loading...</div>; if (isLoading) return <div>Loading...</div>;
if (error) return <div>An error occurred while fetching tasks</div>; if (error) return <div>An error occurred while fetching tasks</div>;
@@ -35,7 +31,7 @@ const ListView = ({ id, setIsModalNewTaskOpen }: Props) => {
/> />
</div> </div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 lg:gap-6"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 lg:gap-6">
{tasks?.map((task: Task) => <TaskCard key={task.id} task={task} />)} {tasks?.map((task: Task) => <TaskCard key={task.taskId} task={task} />)}
</div> </div>
</div> </div>
); );

View File

@@ -7,7 +7,7 @@ import { DataGrid, GridColDef } from "@mui/x-data-grid";
import React from "react"; import React from "react";
type Props = { type Props = {
id: string; projectId: string;
setIsModalNewTaskOpen: (isOpen: boolean) => void; setIsModalNewTaskOpen: (isOpen: boolean) => void;
}; };
@@ -29,7 +29,7 @@ const columns: GridColDef[] = [
renderCell: (params) => ( renderCell: (params) => (
<span <span
className={`inline-flex rounded-full px-2 text-xs font-semibold leading-5 ${ className={`inline-flex rounded-full px-2 text-xs font-semibold leading-5 ${
params.value === "In Progress" params.value === "Work In Progress"
? "bg-green-200 text-green-600" ? "bg-green-200 text-green-600"
: params.value === "Test/Review" : params.value === "Test/Review"
? "bg-green-200 text-green-600" ? "bg-green-200 text-green-600"
@@ -93,13 +93,9 @@ const columns: GridColDef[] = [
}, },
]; ];
const TableView = ({ id, setIsModalNewTaskOpen }: Props) => { const TableView = ({ projectId, setIsModalNewTaskOpen }: Props) => {
const isDarkMode = useAppSelector((state) => state.global.isDarkMode); const isDarkMode = useAppSelector((state) => state.global.isDarkMode);
const { const { data: tasks, error, isLoading } = useGetTasksQuery({ projectId });
data: tasks,
error,
isLoading,
} = useGetTasksQuery({ projectId: Number(id) });
if (isLoading) return <div>Loading...</div>; if (isLoading) return <div>Loading...</div>;
if (error || !tasks) return <div>An error occurred while fetching tasks</div>; if (error || !tasks) return <div>An error occurred while fetching tasks</div>;
@@ -123,6 +119,7 @@ const TableView = ({ id, setIsModalNewTaskOpen }: Props) => {
<DataGrid <DataGrid
rows={tasks || []} rows={tasks || []}
columns={columns} columns={columns}
getRowId={(row) => row.taskId}
className="border border-gray-200 bg-white shadow dark:border-stroke-dark dark:bg-dark-secondary dark:text-gray-200" className="border border-gray-200 bg-white shadow dark:border-stroke-dark dark:bg-dark-secondary dark:text-gray-200"
sx={dataGridSxStyles(isDarkMode)} sx={dataGridSxStyles(isDarkMode)}
/> />

View File

@@ -5,19 +5,15 @@ import "gantt-task-react/dist/index.css";
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
type Props = { type Props = {
id: string; projectId: string;
setIsModalNewTaskOpen: (isOpen: boolean) => void; setIsModalNewTaskOpen: (isOpen: boolean) => void;
}; };
type TaskTypeItems = "task" | "milestone" | "project"; type TaskTypeItems = "task" | "milestone" | "project";
const Timeline = ({ id, setIsModalNewTaskOpen }: Props) => { const Timeline = ({ projectId, setIsModalNewTaskOpen }: Props) => {
const isDarkMode = useAppSelector((state) => state.global.isDarkMode); const isDarkMode = useAppSelector((state) => state.global.isDarkMode);
const { const { data: tasks, error, isLoading } = useGetTasksQuery({ projectId });
data: tasks,
error,
isLoading,
} = useGetTasksQuery({ projectId: Number(id) });
const [displayOptions, setDisplayOptions] = useState<DisplayOption>({ const [displayOptions, setDisplayOptions] = useState<DisplayOption>({
viewMode: ViewMode.Month, viewMode: ViewMode.Month,
@@ -30,7 +26,7 @@ const Timeline = ({ id, setIsModalNewTaskOpen }: Props) => {
start: new Date(task.startDate as string), start: new Date(task.startDate as string),
end: new Date(task.dueDate as string), end: new Date(task.dueDate as string),
name: task.title, name: task.title,
id: `Task-${task.id}`, id: `Task-${task.taskId}`,
type: "task" as TaskTypeItems, type: "task" as TaskTypeItems,
progress: task.points ? (task.points / 10) * 100 : 0, progress: task.points ? (task.points / 10) * 100 : 0,
isDisabled: false, isDisabled: false,

View File

@@ -26,16 +26,19 @@ const Project = ({ params }: Props) => {
/> />
<ProjectHeader activeTab={activeTab} setActiveTab={setActiveTab} /> <ProjectHeader activeTab={activeTab} setActiveTab={setActiveTab} />
{activeTab === "Board" && ( {activeTab === "Board" && (
<Board id={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} /> <Board projectId={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
)} )}
{activeTab === "List" && ( {activeTab === "List" && (
<List id={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} /> <List projectId={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
)} )}
{activeTab === "Timeline" && ( {activeTab === "Timeline" && (
<Timeline id={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} /> <Timeline
projectId={id}
setIsModalNewTaskOpen={setIsModalNewTaskOpen}
/>
)} )}
{activeTab === "Table" && ( {activeTab === "Table" && (
<Table id={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} /> <Table projectId={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
)} )}
</div> </div>
); );

View File

@@ -49,14 +49,14 @@ const Search = () => {
<h2>Tasks</h2> <h2>Tasks</h2>
)} )}
{searchResults.tasks?.map((task) => ( {searchResults.tasks?.map((task) => (
<TaskCard key={task.id} task={task} /> <TaskCard key={task.taskId} task={task} />
))} ))}
{searchResults.projects && searchResults.projects?.length > 0 && ( {searchResults.projects && searchResults.projects?.length > 0 && (
<h2>Projects</h2> <h2>Projects</h2>
)} )}
{searchResults.projects?.map((project) => ( {searchResults.projects?.map((project) => (
<ProjectCard key={project.id} project={project} /> <ProjectCard key={project.projectId} project={project} />
))} ))}
{searchResults.users && searchResults.users?.length > 0 && ( {searchResults.users && searchResults.users?.length > 0 && (

View File

@@ -20,7 +20,7 @@ const CustomToolbar = () => (
); );
const columns: GridColDef[] = [ const columns: GridColDef[] = [
{ field: "id", headerName: "Team ID", width: 100 }, { field: "teamId", headerName: "Team ID", width: 100 },
{ field: "teamName", headerName: "Team Name", width: 200 }, { field: "teamName", headerName: "Team Name", width: 200 },
{ field: "productOwnerUsername", headerName: "Product Owner", width: 200 }, { field: "productOwnerUsername", headerName: "Product Owner", width: 200 },
{ {
@@ -48,6 +48,7 @@ const Teams = () => {
slots={{ slots={{
toolbar: CustomToolbar, toolbar: CustomToolbar,
}} }}
getRowId={(row) => row.teamId}
className="border border-gray-200 bg-white shadow dark:border-stroke-dark dark:bg-dark-secondary dark:text-gray-200" className="border border-gray-200 bg-white shadow dark:border-stroke-dark dark:bg-dark-secondary dark:text-gray-200"
sx={dataGridSxStyles(isDarkMode)} sx={dataGridSxStyles(isDarkMode)}
/> />

View File

@@ -24,7 +24,7 @@ const Timeline = () => {
start: new Date(project.startDate as string), start: new Date(project.startDate as string),
end: new Date(project.endDate as string), end: new Date(project.endDate as string),
name: project.name, name: project.name,
id: `Project-${project.id}`, id: `Project-${project.projectId}`,
type: "project" as TaskTypeItems, type: "project" as TaskTypeItems,
progress: 50, progress: 50,
isDisabled: false, isDisabled: false,

View File

@@ -31,7 +31,7 @@ const columns: GridColDef[] = [
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<div className="h-9 w-9"> <div className="h-9 w-9">
<Image <Image
src={`/${params.value}`} src={`${process.env.S3_PUBLIC_IMAGE_URL}/${params.value}`}
alt={params.row.username} alt={params.row.username}
width={100} width={100}
height={50} height={50}

View File

@@ -80,9 +80,9 @@ export const api = createApi({
baseUrl: process.env.NEXT_PUBLIC_API_BASE_URL, baseUrl: process.env.NEXT_PUBLIC_API_BASE_URL,
prepareHeaders: async (headers) => { prepareHeaders: async (headers) => {
const session = await fetchAuthSession(); const session = await fetchAuthSession();
const { accessToken } = session.tokens ?? {}; const { idToken } = session.tokens ?? {};
if (accessToken) { if (idToken) {
headers.set("Authorization", `Bearer ${accessToken}`); headers.set("Authorization", `Bearer ${idToken}`);
} }
return headers; return headers;
}, },
@@ -120,7 +120,7 @@ export const api = createApi({
}), }),
invalidatesTags: ["Projects"], invalidatesTags: ["Projects"],
}), }),
getTasks: build.query<Task[], { projectId: number }>({ getTasks: build.query<Task[], { projectId: string }>({
query: ({ projectId }) => `tasks?projectId=${projectId}`, query: ({ projectId }) => `tasks?projectId=${projectId}`,
providesTags: (result) => providesTags: (result) =>
result result

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
{
"name": "nodejs",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.699.0",
"@aws-sdk/lib-dynamodb": "^3.699.0",
"@types/uuid": "^10.0.0",
"uuid": "^11.0.3"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,18 +5,27 @@
"scripts": { "scripts": {
"infra:init": "cd terraform && AWS_PROFILE=default terraform init", "infra:init": "cd terraform && AWS_PROFILE=default terraform init",
"infra:plan": "cd terraform && AWS_PROFILE=default terraform plan", "infra:plan": "cd terraform && AWS_PROFILE=default terraform plan",
"infra:apply": "cd terraform && AWS_PROFILE=default terraform apply" "infra:apply": "cd terraform && AWS_PROFILE=default terraform apply",
"infra:destroy": "cd terraform && AWS_PROFILE=default terraform destroy",
"sls:package": "AWS_PROFILE=default sls package",
"sls:deploy": "AWS_PROFILE=default sls deploy",
"sls:remove": "AWS_PROFILE=default sls remove"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"dependencies": { "dependencies": {
"aws-sdk": "^2.1692.0" "@aws-sdk/client-dynamodb": "^3.699.0",
"@aws-sdk/lib-dynamodb": "^3.699.0",
"@types/uuid": "^10.0.0",
"aws-sdk": "^2.1692.0",
"uuid": "^11.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.9.1", "@types/node": "^22.9.1",
"eslint": "^9.15.0", "eslint": "^9.15.0",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"serverless-prune-plugin": "^2.1.0",
"typescript": "^5.6.3" "typescript": "^5.6.3"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,179 @@
service: tasker-server
plugins:
- serverless-prune-plugin
provider:
stackName: ${self:service}
name: aws
region: "eu-north-1"
runtime: nodejs20.x
environment:
SLS_REGION: ${self:provider.region}
API_BASE_URL: ${ssm:/tasker/api/base-url}
TASKER_TASK_TABLE_NAME: ${ssm:/tasker/dynamodb/task-table-name}
TASKER_PROJECT_TABLE_NAME: ${ssm:/tasker/dynamodb/project-table-name}
TASKER_USER_TABLE_NAME: ${ssm:/tasker/dynamodb/user-table-name}
TASKER_TASK_EXTRA_TABLE_NAME: ${ssm:/tasker/dynamodb/task-extra-table-name}
TASKER_TEAM_TABLE_NAME: ${ssm:/tasker/dynamodb/team-table-name}
layers:
- ${ssm:/tasker/layers/tasker-layer-arn}
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
Resource:
[
"arn:aws:dynamodb:${self:provider.region}:*:table/tasker-*",
"arn:aws:dynamodb:${self:provider.region}:*:table/tasker-*/*",
]
- Effect: Allow
Action:
- execute-api:Invoke
Resource:
- "arn:aws:execute-api:${self:provider.region}:*:*/*/POST/users"
functions:
populateSeedData:
handler: seed/populateSeedData.handler
memorySize: 1024
timeout: 60
# POST /users or triggered by Cognito
createUser:
handler: src/handlers/createUser.handler
memorySize: 1024
timeout: 60
events:
- http:
path: users
method: post
cors: true
authorizer: aws_iam
- cognitoUserPool:
existing: true
pool: ${ssm:/tasker/cognito/user-pool-name}
trigger: PostConfirmation
# POST /projects
createProject:
handler: src/handlers/createProject.handler
memorySize: 1024
timeout: 60
events:
- http:
path: projects
method: post
cors: true
authorizer:
type: COGNITO_USER_POOLS
arn: ${ssm:/tasker/cognito/user-pool-arn}
# POST /tasks
createTask:
handler: src/handlers/createTask.handler
memorySize: 1024
timeout: 60
events:
- http:
path: tasks
method: post
cors: true
authorizer:
type: COGNITO_USER_POOLS
arn: ${ssm:/tasker/cognito/user-pool-arn}
# GET /projects
getProjects:
handler: src/handlers/getProjects.handler
memorySize: 1024
timeout: 60
events:
- http:
path: projects
method: get
cors: true
authorizer:
type: COGNITO_USER_POOLS
arn: ${ssm:/tasker/cognito/user-pool-arn}
# GET /tasks?projectId=
getTasks:
handler: src/handlers/getTasks.handler
memorySize: 1024
timeout: 60
events:
- http:
path: tasks
method: get
cors: true
authorizer:
type: COGNITO_USER_POOLS
arn: ${ssm:/tasker/cognito/user-pool-arn}
# GET /teams
getTeams:
handler: src/handlers/getTeams.handler
memorySize: 1024
timeout: 60
events:
- http:
path: teams
method: get
cors: true
authorizer:
type: COGNITO_USER_POOLS
arn: ${ssm:/tasker/cognito/user-pool-arn}
# GET /users
getUsers:
handler: src/handlers/getUsers.handler
memorySize: 1024
timeout: 60
events:
- http:
path: users
method: get
cors: true
authorizer:
type: COGNITO_USER_POOLS
arn: ${ssm:/tasker/cognito/user-pool-arn}
# GET /users/{cognitoId}
getUser:
handler: src/handlers/getUser.handler
memorySize: 1024
timeout: 60
events:
- http:
path: users/{cognitoId}
method: get
cors: true
authorizer:
type: COGNITO_USER_POOLS
arn: ${ssm:/tasker/cognito/user-pool-arn}
# GET /tasks/user/${userId}
getUserTasks:
handler: src/handlers/getUserTasks.handler
memorySize: 1024
timeout: 60
events:
- http:
path: tasks/user/{userId}
method: get
cors: true
authorizer:
type: COGNITO_USER_POOLS
arn: ${ssm:/tasker/cognito/user-pool-arn}
# PATCH /tasks/{taskId}/status
updateTaskStatus:
handler: src/handlers/updateTaskStatus.handler
memorySize: 1024
timeout: 60
events:
- http:
path: tasks/{taskId}/status
method: patch
cors: true
authorizer:
type: COGNITO_USER_POOLS
arn: ${ssm:/tasker/cognito/user-pool-arn}

View File

@@ -0,0 +1,51 @@
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, PutCommandInput } from "@aws-sdk/lib-dynamodb";
import { v4 as uuidv4 } from "uuid";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_PROJECT_TABLE_NAME = process.env.TASKER_PROJECT_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
const { name, description, startDate, endDate } = JSON.parse(event.body);
try {
const newProject = {
category: "projects",
projectId: `project_${uuidv4()}`,
name,
description,
startDate,
endDate,
};
const params: PutCommandInput = {
TableName: TASKER_PROJECT_TABLE_NAME,
Item: newProject,
};
await docClient.put(params);
return {
statusCode: 201,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(newProject),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error creating project: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,69 @@
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, PutCommandInput } from "@aws-sdk/lib-dynamodb";
import { v4 as uuidv4 } from "uuid";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_TASK_TABLE_NAME = process.env.TASKER_TASK_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
const {
title,
description,
status,
priority,
tags,
startDate,
dueDate,
points,
projectId,
authorUserId,
assignedUserId,
} = JSON.parse(event.body);
try {
const newTask = {
category: "tasks",
taskId: `task_${uuidv4()}`,
title,
description,
status,
priority,
tags,
startDate,
dueDate,
points,
projectId,
authorUserId,
assignedUserId,
};
const params: PutCommandInput = {
TableName: TASKER_TASK_TABLE_NAME,
Item: newTask,
};
await docClient.put(params);
return {
statusCode: 201,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(newTask),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error creating task: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,42 @@
import { fetchRandomTeamId } from "@/lib/util";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, PutCommandInput } from "@aws-sdk/lib-dynamodb";
import { v4 as uuidv4 } from "uuid";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_USER_TABLE_NAME = process.env.TASKER_USER_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
console.info(`Event: ${JSON.stringify(event)}`);
const username =
event.request.userAttributes["preferred_username"] || event.userName;
const cognitoId = event.userName;
const teamId = await fetchRandomTeamId();
try {
const newUser = {
category: "users",
cognitoId,
userId: `user_${uuidv4()}`,
username,
profilePictureUrl: "p0.jpeg",
teamId,
};
const params: PutCommandInput = {
TableName: TASKER_USER_TABLE_NAME,
Item: newUser,
};
await docClient.put(params);
console.info(`User ${username} created with teamId ${teamId}`);
} catch (error: any) {
throw new Error(`Error creating user: ${error.message}`);
}
return event;
};

View File

@@ -0,0 +1,42 @@
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, QueryCommandInput } from "@aws-sdk/lib-dynamodb";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_PROJECT_TABLE_NAME = process.env.TASKER_PROJECT_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
try {
const params: QueryCommandInput = {
TableName: TASKER_PROJECT_TABLE_NAME,
KeyConditionExpression: "category = :category",
ExpressionAttributeValues: {
":category": "projects",
},
};
const projects = await docClient.query(params);
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(projects.Items),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error retrieving projects: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,75 @@
import {
fetchAttachments,
fetchComments,
fetchUserWithUserId,
} from "@/lib/util";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, QueryCommandInput } from "@aws-sdk/lib-dynamodb";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_TASK_TABLE_NAME = process.env.TASKER_TASK_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
const { projectId } = event.queryStringParameters;
try {
const params: QueryCommandInput = {
TableName: TASKER_TASK_TABLE_NAME,
KeyConditionExpression: "category = :category AND projectId = :projectId",
IndexName: "GSI-project-id",
ExpressionAttributeValues: {
":category": "tasks",
":projectId": projectId,
},
};
const result = await docClient.query(params);
const tasks = result.Items || [];
const tasksWithDetails = await Promise.all(
tasks.map(async (task: any) => {
const author = task.authorUserId
? await fetchUserWithUserId(task.authorUserId)
: null;
const assignee = task.assignedUserId
? await fetchUserWithUserId(task.assignedUserId)
: null;
const comments = await fetchComments(task.taskId);
const attachments = await fetchAttachments(task.taskId);
return {
...task,
author,
assignee,
comments,
attachments,
};
})
);
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(tasksWithDetails),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error retrieving tasks: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,62 @@
import { fetchUserWithUserId } from "@/lib/util";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, QueryCommandInput } from "@aws-sdk/lib-dynamodb";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_TEAM_TABLE_NAME = process.env.TASKER_TEAM_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
try {
const params: QueryCommandInput = {
TableName: TASKER_TEAM_TABLE_NAME,
KeyConditionExpression: "category = :category",
ExpressionAttributeValues: {
":category": "teams",
},
};
const result = await docClient.query(params);
const teams = result.Items || [];
const teamsWithUsernames = await Promise.all(
teams.map(async (team: any) => {
const productOwnerUsername = team.productOwnerUserId
? (await fetchUserWithUserId(team.productOwnerUserId))?.username
: null;
const projectManagerUsername = team.projectManagerUserId
? (await fetchUserWithUserId(team.projectManagerUserId))?.username
: null;
return {
...team,
productOwnerUsername,
projectManagerUsername,
};
})
);
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(teamsWithUsernames),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error retrieving teams: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,44 @@
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, QueryCommandInput } from "@aws-sdk/lib-dynamodb";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_USER_TABLE_NAME = process.env.TASKER_USER_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
const { cognitoId } = event.pathParameters;
try {
const params: QueryCommandInput = {
TableName: TASKER_USER_TABLE_NAME,
KeyConditionExpression: "category = :category AND cognitoId = :cognitoId",
ExpressionAttributeValues: {
":category": "users",
":cognitoId": cognitoId,
},
};
const user = await docClient.query(params);
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(user.Items?.[0] || {}),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error retrieving user: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,40 @@
import { queryTasks } from "@/lib/util";
export const handler = async (event: any): Promise<any> => {
const { userId } = event.pathParameters;
try {
const authorTasks = await queryTasks(
userId,
"GSI-author-user-id",
"authorUserId"
);
const assigneeTasks = await queryTasks(
userId,
"GSI-assigned-user-id",
"assignedUserId"
);
const userTasks = [...authorTasks, ...assigneeTasks];
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(userTasks),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error retrieving tasks for user: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,42 @@
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, QueryCommandInput } from "@aws-sdk/lib-dynamodb";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_USER_TABLE_NAME = process.env.TASKER_USER_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
try {
const params: QueryCommandInput = {
TableName: TASKER_USER_TABLE_NAME,
KeyConditionExpression: "category = :category",
ExpressionAttributeValues: {
":category": "users",
},
};
const users = await docClient.query(params);
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(users.Items),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error retrieving users: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,52 @@
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, UpdateCommandInput } from "@aws-sdk/lib-dynamodb";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_TASK_TABLE_NAME = process.env.TASKER_TASK_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const handler = async (event: any): Promise<any> => {
const { taskId } = event.pathParameters;
const { status } = JSON.parse(event.body);
try {
const params: UpdateCommandInput = {
TableName: TASKER_TASK_TABLE_NAME,
Key: {
category: "tasks",
taskId,
},
UpdateExpression: "set #status = :status",
ExpressionAttributeNames: {
"#status": "status",
},
ExpressionAttributeValues: {
":status": status,
},
ReturnValues: "ALL_NEW",
};
const updatedTask = await docClient.update(params);
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(updatedTask.Attributes),
};
} catch (error: any) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
message: `Error updating task: ${error.message}`,
}),
};
}
};

View File

@@ -0,0 +1,97 @@
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, QueryCommandInput } from "@aws-sdk/lib-dynamodb";
const SLS_REGION = process.env.SLS_REGION;
const TASKER_TEAM_TABLE_NAME = process.env.TASKER_TEAM_TABLE_NAME || "";
const TASKER_USER_TABLE_NAME = process.env.TASKER_USER_TABLE_NAME || "";
const TASKER_TASK_TABLE_NAME = process.env.TASKER_TASK_TABLE_NAME || "";
const TASKER_TASK_EXTRA_TABLE_NAME =
process.env.TASKER_TASK_EXTRA_TABLE_NAME || "";
const client = new DynamoDBClient({ region: SLS_REGION });
const docClient = DynamoDBDocument.from(client);
export const fetchRandomTeamId = async () => {
const params = {
TableName: TASKER_TEAM_TABLE_NAME,
KeyConditionExpression: "category = :category",
ExpressionAttributeValues: {
":category": "teams",
},
};
const teams = await docClient.query(params);
if (!teams.Items) {
return null;
}
const randomTeam =
teams.Items[Math.floor(Math.random() * teams.Items.length)];
return randomTeam.teamId;
};
export const fetchUserWithUserId = async (userId: string): Promise<any> => {
const params: QueryCommandInput = {
TableName: TASKER_USER_TABLE_NAME,
KeyConditionExpression: "category = :category AND userId = :userId",
IndexName: "GSI-user-id",
ExpressionAttributeValues: {
":category": "users",
":userId": userId,
},
};
const result = await docClient.query(params);
return result.Items?.[0];
};
export const fetchComments = async (taskId: string): Promise<any> => {
const params: QueryCommandInput = {
TableName: TASKER_TASK_EXTRA_TABLE_NAME,
KeyConditionExpression: "category = :category AND taskId = :taskId",
IndexName: "GSI-task-id",
ExpressionAttributeValues: {
":category": "comments",
":taskId": taskId,
},
};
const result = await docClient.query(params);
return result.Items;
};
export const fetchAttachments = async (taskId: string): Promise<any> => {
const params: QueryCommandInput = {
TableName: TASKER_TASK_EXTRA_TABLE_NAME,
KeyConditionExpression: "category = :category AND taskId = :taskId",
IndexName: "GSI-task-id",
ExpressionAttributeValues: {
":category": "attachments",
":taskId": taskId,
},
};
const result = await docClient.query(params);
return result.Items;
};
export const queryTasks = async (
userId: string,
indexName: string,
key: string
): Promise<any> => {
const params: QueryCommandInput = {
TableName: TASKER_TASK_TABLE_NAME,
KeyConditionExpression: "category = :category AND #key = :userId",
IndexName: indexName,
ExpressionAttributeValues: {
":category": "tasks",
":userId": userId,
},
ExpressionAttributeNames: {
"#key": key,
},
};
const result = await docClient.query(params);
return result.Items ?? [];
};

View File

@@ -76,6 +76,20 @@ resource "aws_ssm_parameter" "user_pool_id" {
value = aws_cognito_user_pool.tasker_cognito_user_pool.id value = aws_cognito_user_pool.tasker_cognito_user_pool.id
} }
resource "aws_ssm_parameter" "user_pool_arn" {
name = "/tasker/cognito/user-pool-arn"
description = "Tasker Cognito User Pool ARN"
type = "String"
value = aws_cognito_user_pool.tasker_cognito_user_pool.arn
}
resource "aws_ssm_parameter" "user_pool_name" {
name = "/tasker/cognito/user-pool-name"
description = "Tasker Cognito User Pool Name"
type = "String"
value = aws_cognito_user_pool.tasker_cognito_user_pool.name
}
resource "aws_ssm_parameter" "client_id" { resource "aws_ssm_parameter" "client_id" {
name = "/tasker/cognito/client-id" name = "/tasker/cognito/client-id"
description = "Tasker Cognito Client ID" description = "Tasker Cognito Client ID"

View File

@@ -1,11 +1,11 @@
resource "aws_dynamodb_table" "tasker_project_table" { resource "aws_dynamodb_table" "tasker_project_table" {
name = "tasker-project-table" name = "tasker-project-table"
billing_mode = "PAY_PER_REQUEST" billing_mode = "PAY_PER_REQUEST"
hash_key = "type" hash_key = "category"
range_key = "projectId" range_key = "projectId"
attribute { attribute {
name = "type" name = "category"
type = "S" type = "S"
} }
@@ -30,11 +30,11 @@ resource "aws_ssm_parameter" "tasker_project_table_arn" {
resource "aws_dynamodb_table" "tasker_user_table" { resource "aws_dynamodb_table" "tasker_user_table" {
name = "tasker-user-table" name = "tasker-user-table"
billing_mode = "PAY_PER_REQUEST" billing_mode = "PAY_PER_REQUEST"
hash_key = "type" hash_key = "category"
range_key = "cognitoId" range_key = "cognitoId"
attribute { attribute {
name = "type" name = "category"
type = "S" type = "S"
} }
@@ -50,7 +50,7 @@ resource "aws_dynamodb_table" "tasker_user_table" {
global_secondary_index { global_secondary_index {
name = "GSI-user-id" name = "GSI-user-id"
hash_key = "type" hash_key = "category"
range_key = "userId" range_key = "userId"
projection_type = "ALL" projection_type = "ALL"
} }
@@ -71,11 +71,11 @@ resource "aws_ssm_parameter" "tasker_user_table_arn" {
resource "aws_dynamodb_table" "tasker_team_table" { resource "aws_dynamodb_table" "tasker_team_table" {
name = "tasker-team-table" name = "tasker-team-table"
billing_mode = "PAY_PER_REQUEST" billing_mode = "PAY_PER_REQUEST"
hash_key = "type" hash_key = "category"
range_key = "teamId" range_key = "teamId"
attribute { attribute {
name = "type" name = "category"
type = "S" type = "S"
} }
@@ -100,11 +100,11 @@ resource "aws_ssm_parameter" "tasker_team_table_arn" {
resource "aws_dynamodb_table" "tasker_task_table" { resource "aws_dynamodb_table" "tasker_task_table" {
name = "tasker-task-table" name = "tasker-task-table"
billing_mode = "PAY_PER_REQUEST" billing_mode = "PAY_PER_REQUEST"
hash_key = "type" hash_key = "category"
range_key = "taskId" range_key = "taskId"
attribute { attribute {
name = "type" name = "category"
type = "S" type = "S"
} }
@@ -130,21 +130,21 @@ resource "aws_dynamodb_table" "tasker_task_table" {
global_secondary_index { global_secondary_index {
name = "GSI-project-id" name = "GSI-project-id"
hash_key = "type" hash_key = "category"
range_key = "projectId" range_key = "projectId"
projection_type = "ALL" projection_type = "ALL"
} }
global_secondary_index { global_secondary_index {
name = "GSI-author-user-id" name = "GSI-author-user-id"
hash_key = "type" hash_key = "category"
range_key = "authorUserId" range_key = "authorUserId"
projection_type = "ALL" projection_type = "ALL"
} }
global_secondary_index { global_secondary_index {
name = "GSI-assigned-user-id" name = "GSI-assigned-user-id"
hash_key = "type" hash_key = "category"
range_key = "assignedUserId" range_key = "assignedUserId"
projection_type = "ALL" projection_type = "ALL"
} }
@@ -165,11 +165,11 @@ resource "aws_ssm_parameter" "tasker_task_table_arn" {
resource "aws_dynamodb_table" "tasker_task_extra_table" { resource "aws_dynamodb_table" "tasker_task_extra_table" {
name = "tasker-task-extra-table" name = "tasker-task-extra-table"
billing_mode = "PAY_PER_REQUEST" billing_mode = "PAY_PER_REQUEST"
hash_key = "type" hash_key = "category"
range_key = "id" range_key = "id"
attribute { attribute {
name = "type" name = "category"
type = "S" type = "S"
} }
@@ -185,7 +185,7 @@ resource "aws_dynamodb_table" "tasker_task_extra_table" {
global_secondary_index { global_secondary_index {
name = "GSI-task-id" name = "GSI-task-id"
hash_key = "type" hash_key = "category"
range_key = "taskId" range_key = "taskId"
projection_type = "ALL" projection_type = "ALL"
} }

View File

@@ -0,0 +1,24 @@
resource "aws_s3_bucket" "tasker_lambda_layer" {
bucket = "tasker-lambda-layer"
}
resource "aws_s3_object" "tasker_lambda_layer_zip" {
bucket = aws_s3_bucket.tasker_lambda_layer.id
key = "layers/tasker-layer.zip"
source = "../layers/tasker-layer.zip"
}
resource "aws_lambda_layer_version" "tasker_layer" {
layer_name = "tasker-layer"
s3_bucket = aws_s3_object.tasker_lambda_layer_zip.bucket
s3_key = aws_s3_object.tasker_lambda_layer_zip.key
compatible_runtimes = ["nodejs20.x"]
description = "Tasker Lambda Layer with shared dependencies"
}
resource "aws_ssm_parameter" "tasker_layer_arn" {
name = "/tasker/layers/tasker-layer-arn"
type = "String"
value = aws_lambda_layer_version.tasker_layer.arn
}

View File

@@ -1,11 +1,22 @@
resource "aws_s3_bucket" "tasker_public_images" { resource "aws_s3_bucket" "tasker_public_images" {
bucket = "tasker-public-images" bucket = "tasker-public-images"
tags = {
Environment = "Dev"
}
} }
resource "aws_s3_bucket_policy" "public_read_policy" {
bucket = aws_s3_bucket.tasker_public_images.id
policy = data.aws_iam_policy_document.public_read_policy.json
}
data "aws_iam_policy_document" "public_read_policy" {
statement {
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.tasker_public_images.arn}/*"]
principals {
type = "AWS"
identifiers = ["*"]
}
}
}
resource "aws_s3_bucket_ownership_controls" "tasker_public_images_ownership_controls" { resource "aws_s3_bucket_ownership_controls" "tasker_public_images_ownership_controls" {
bucket = aws_s3_bucket.tasker_public_images.id bucket = aws_s3_bucket.tasker_public_images.id