Wip backend (#4)
* feat: Add new API handlers for user, project, and task management; update package dependencies * feat: Update .gitignore, add Lambda layer configuration, and refactor DynamoDB handlers to use AWS SDK v3 * feat: Update serverless configuration and refactor API handlers to improve error handling and response structure * feat: Add Cognito user pool name parameter and update API handlers to include CORS headers * feat: Update task and project ID formats, add populateSeedData function, and enhance user ID handling * feat: Update image source paths to use S3 public URL for profile and task attachments
This commit was merged in pull request #4.
This commit is contained in:
10
.gitignore
vendored
10
.gitignore
vendored
@@ -22,7 +22,13 @@
|
||||
**/next-env.d.ts
|
||||
|
||||
# images
|
||||
**/public/**
|
||||
tasker-client/public/**
|
||||
|
||||
# terraform
|
||||
**/**/.terraform**
|
||||
**/.terraform**
|
||||
|
||||
# serverless
|
||||
**/.serverless/**
|
||||
|
||||
# zipped layers
|
||||
**/layers/**.zip
|
||||
|
||||
@@ -23,7 +23,14 @@ const ModalNewTask = ({ isOpen, onClose, id = null }: Props) => {
|
||||
const [projectId, setProjectId] = useState("");
|
||||
|
||||
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), {
|
||||
representation: "complete",
|
||||
@@ -40,14 +47,17 @@ const ModalNewTask = ({ isOpen, onClose, id = null }: Props) => {
|
||||
tags,
|
||||
startDate: formattedStartDate,
|
||||
dueDate: formattedDueDate,
|
||||
authorUserId: parseInt(authorUserId),
|
||||
assignedUserId: parseInt(assignedUserId),
|
||||
projectId: id !== null ? Number(id) : Number(projectId),
|
||||
authorUserId: authorUserId,
|
||||
assignedUserId: assignedUserId,
|
||||
projectId: id !== null ? id : projectId,
|
||||
});
|
||||
};
|
||||
|
||||
const isFormValid = () => {
|
||||
return title && authorUserId && !(id !== null || projectId);
|
||||
console.log(title, authorUserId, id, projectId);
|
||||
return (
|
||||
title && authorUserId && assignedUserId && (id !== null || projectId)
|
||||
);
|
||||
};
|
||||
|
||||
const selectStyles =
|
||||
@@ -87,7 +97,7 @@ const ModalNewTask = ({ isOpen, onClose, id = null }: Props) => {
|
||||
}
|
||||
>
|
||||
<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.TestReview}>Test/Review</option>
|
||||
<option value={Status.Done}>Done</option>
|
||||
|
||||
@@ -77,7 +77,7 @@ const Navbar = () => {
|
||||
<div className="align-center flex h-9 w-9 justify-center">
|
||||
{!!currentUserDetails?.profilePictureUrl ? (
|
||||
<Image
|
||||
src={`/${currentUserDetails?.profilePictureUrl}`}
|
||||
src={`${process.env.S3_PUBLIC_IMAGE_URL}/${currentUserDetails?.profilePictureUrl}`}
|
||||
alt={currentUserDetails?.username || "User Profile Picture"}
|
||||
width={100}
|
||||
height={50}
|
||||
|
||||
@@ -70,7 +70,12 @@ const Sidebar = () => {
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
<h3 className="text-md font-bold tracking-widest dark:text-gray-200">
|
||||
Tasker.IO
|
||||
@@ -93,7 +98,6 @@ const Sidebar = () => {
|
||||
<button
|
||||
onClick={() =>
|
||||
setShowProjects((prev) => {
|
||||
console.log(prev);
|
||||
return !prev;
|
||||
})
|
||||
}
|
||||
@@ -109,10 +113,10 @@ const Sidebar = () => {
|
||||
{showProjects &&
|
||||
projects?.map((project) => (
|
||||
<SidebarLink
|
||||
key={project.id}
|
||||
key={project.projectId}
|
||||
icon={Briefcase}
|
||||
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">
|
||||
{!!currentUserDetails?.profilePictureUrl ? (
|
||||
<Image
|
||||
src={`/${currentUserDetails?.profilePictureUrl}`}
|
||||
src={`${process.env.S3_PUBLIC_IMAGE_URL}/${currentUserDetails?.profilePictureUrl}`}
|
||||
alt={currentUserDetails?.username || "User Profile Picture"}
|
||||
width={100}
|
||||
height={50}
|
||||
|
||||
@@ -16,7 +16,7 @@ const TaskCard = ({ task }: Props) => {
|
||||
<div className="flex flex-wrap">
|
||||
{task.attachments && task.attachments.length > 0 && (
|
||||
<Image
|
||||
src={`/${task.attachments[0].fileURL}`}
|
||||
src={`${process.env.S3_PUBLIC_IMAGE_URL}/${task.attachments[0].fileURL}`}
|
||||
alt={task.attachments[0].fileName}
|
||||
width={400}
|
||||
height={200}
|
||||
@@ -27,7 +27,7 @@ const TaskCard = ({ task }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
<p>
|
||||
<strong>ID:</strong> {task.id}
|
||||
<strong>ID:</strong> {task.taskId}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Title:</strong> {task.title}
|
||||
|
||||
@@ -11,7 +11,7 @@ const UserCard = ({ user }: Props) => {
|
||||
<div className="flex items-center rounded border p-4 shadow">
|
||||
{user.profilePictureUrl && (
|
||||
<Image
|
||||
src={`/${user.profilePictureUrl}`}
|
||||
src={`${process.env.S3_PUBLIC_IMAGE_URL}/${user.profilePictureUrl}`}
|
||||
alt="profile picture"
|
||||
width={32}
|
||||
height={32}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Priority,
|
||||
Project,
|
||||
Task,
|
||||
useGetAuthUserQuery,
|
||||
useGetProjectsQuery,
|
||||
useGetTasksByUserQuery,
|
||||
} from "@/state/api";
|
||||
@@ -77,12 +78,13 @@ const taskColumns: GridColDef[] = [
|
||||
const statusColors = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042"];
|
||||
|
||||
const HomePage = () => {
|
||||
const userId = 1;
|
||||
const { data: currentUser } = useGetAuthUserQuery({});
|
||||
const userId = currentUser?.userDetails?.userId ?? null;
|
||||
const {
|
||||
data: tasks,
|
||||
isLoading: tasksLoading,
|
||||
isError: tasksError,
|
||||
} = useGetTasksByUserQuery(userId || 0, {
|
||||
} = useGetTasksByUserQuery(userId || "", {
|
||||
skip: userId === null,
|
||||
});
|
||||
const { data: projects, isLoading: isProjectsLoading } =
|
||||
@@ -191,6 +193,7 @@ const HomePage = () => {
|
||||
columns={taskColumns}
|
||||
checkboxSelection
|
||||
loading={tasksLoading}
|
||||
getRowId={(row) => row.taskId}
|
||||
getRowClassName={() => "data-grid-row"}
|
||||
getCellClassName={() => "data-grid-cell"}
|
||||
className="border border-gray-200 bg-white shadow dark:border-stroke-dark dark:bg-dark-secondary dark:text-gray-200"
|
||||
|
||||
@@ -83,7 +83,7 @@ const ReusablePriorityPage = ({ priority }: Props) => {
|
||||
data: tasks,
|
||||
isLoading,
|
||||
isError: isTasksError,
|
||||
} = useGetTasksByUserQuery(userId || 0, {
|
||||
} = useGetTasksByUserQuery(userId || "", {
|
||||
skip: userId === null,
|
||||
});
|
||||
|
||||
@@ -135,7 +135,7 @@ const ReusablePriorityPage = ({ priority }: Props) => {
|
||||
) : view === "list" ? (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{filteredTasks?.map((task: Task) => (
|
||||
<TaskCard key={task.id} task={task} />
|
||||
<TaskCard key={task.taskId} task={task} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
@@ -146,7 +146,7 @@ const ReusablePriorityPage = ({ priority }: Props) => {
|
||||
rows={filteredTasks}
|
||||
columns={columns}
|
||||
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"
|
||||
sx={dataGridSxStyles(isDarkMode)}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useGetTasksQuery, useUpdateTaskStatusMutation } from "@/state/api";
|
||||
import React from "react";
|
||||
import {
|
||||
Status,
|
||||
useGetTasksQuery,
|
||||
useUpdateTaskStatusMutation,
|
||||
} from "@/state/api";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { DndProvider, useDrag, useDrop } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import { Task as TaskType } from "@/state/api";
|
||||
@@ -9,22 +13,40 @@ import { format } from "date-fns";
|
||||
import Image from "next/image";
|
||||
|
||||
type BoardProps = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
setIsModalNewTaskOpen: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
const taskStatus = ["To Do", "In Progress", "Test/Review", "Done"];
|
||||
|
||||
const BoardView = ({ id, setIsModalNewTaskOpen }: BoardProps) => {
|
||||
const BoardView = ({ projectId, setIsModalNewTaskOpen }: BoardProps) => {
|
||||
const {
|
||||
data: tasks,
|
||||
data: fetchedTasks,
|
||||
isLoading,
|
||||
error,
|
||||
} = useGetTasksQuery({ projectId: Number(id) });
|
||||
} = useGetTasksQuery({ projectId });
|
||||
const [updateTaskStatus] = useUpdateTaskStatusMutation();
|
||||
const [tasks, setTasks] = useState<TaskType[]>([]);
|
||||
|
||||
const moveTask = (taskId: number, toStatus: string) => {
|
||||
updateTaskStatus({ taskId, status: toStatus });
|
||||
useEffect(() => {
|
||||
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>;
|
||||
@@ -50,7 +72,7 @@ const BoardView = ({ id, setIsModalNewTaskOpen }: BoardProps) => {
|
||||
type TaskColumnProps = {
|
||||
status: string;
|
||||
tasks: TaskType[];
|
||||
moveTask: (taskId: number, toStatus: string) => void;
|
||||
moveTask: (taskId: string, toStatus: string) => void;
|
||||
setIsModalNewTaskOpen: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
@@ -62,7 +84,7 @@ const TaskColumn = ({
|
||||
}: TaskColumnProps) => {
|
||||
const [{ isOver }, drop] = useDrop(() => ({
|
||||
accept: "task",
|
||||
drop: (item: { id: number }) => moveTask(item.id, status),
|
||||
drop: (item: { id: string }) => moveTask(item.id, status),
|
||||
collect: (monitor: any) => ({
|
||||
isOver: !!monitor.isOver(),
|
||||
}),
|
||||
@@ -116,7 +138,7 @@ const TaskColumn = ({
|
||||
{tasks
|
||||
.filter((task) => task.status === status)
|
||||
.map((task) => (
|
||||
<Task key={task.id} task={task} />
|
||||
<Task key={task.taskId} task={task} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -129,7 +151,7 @@ type TaskProps = {
|
||||
const Task = ({ task }: TaskProps) => {
|
||||
const [{ isDragging }, drag] = useDrag(() => ({
|
||||
type: "task",
|
||||
item: { id: task.id },
|
||||
item: { id: task.taskId },
|
||||
collect: (monitor: any) => ({
|
||||
isDragging: !!monitor.isDragging(),
|
||||
}),
|
||||
@@ -175,7 +197,7 @@ const Task = ({ task }: TaskProps) => {
|
||||
>
|
||||
{task.attachments && task.attachments.length > 0 && (
|
||||
<Image
|
||||
src={`/${task.attachments[0].fileURL}`}
|
||||
src={`${process.env.S3_PUBLIC_IMAGE_URL}/${task.attachments[0].fileURL}`}
|
||||
alt={task.attachments[0].fileName}
|
||||
width={400}
|
||||
height={200}
|
||||
@@ -227,7 +249,7 @@ const Task = ({ task }: TaskProps) => {
|
||||
{task.assignee && (
|
||||
<Image
|
||||
key={task.assignee.userId}
|
||||
src={`/${task.assignee.profilePictureUrl!}`}
|
||||
src={`${process.env.S3_PUBLIC_IMAGE_URL}/${task.assignee.profilePictureUrl!}`}
|
||||
alt={task.assignee.username}
|
||||
width={30}
|
||||
height={30}
|
||||
@@ -237,7 +259,7 @@ const Task = ({ task }: TaskProps) => {
|
||||
{task.author && (
|
||||
<Image
|
||||
key={task.author.userId}
|
||||
src={`/${task.author.profilePictureUrl!}`}
|
||||
src={`${process.env.S3_PUBLIC_IMAGE_URL}/${task.author.profilePictureUrl!}`}
|
||||
alt={task.author.username}
|
||||
width={30}
|
||||
height={30}
|
||||
|
||||
@@ -4,16 +4,12 @@ import { Task, useGetTasksQuery } from "@/state/api";
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
setIsModalNewTaskOpen: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
const ListView = ({ id, setIsModalNewTaskOpen }: Props) => {
|
||||
const {
|
||||
data: tasks,
|
||||
error,
|
||||
isLoading,
|
||||
} = useGetTasksQuery({ projectId: Number(id) });
|
||||
const ListView = ({ projectId, setIsModalNewTaskOpen }: Props) => {
|
||||
const { data: tasks, error, isLoading } = useGetTasksQuery({ projectId });
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error) return <div>An error occurred while fetching tasks</div>;
|
||||
@@ -35,7 +31,7 @@ const ListView = ({ id, setIsModalNewTaskOpen }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
setIsModalNewTaskOpen: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ const columns: GridColDef[] = [
|
||||
renderCell: (params) => (
|
||||
<span
|
||||
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"
|
||||
: params.value === "Test/Review"
|
||||
? "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 {
|
||||
data: tasks,
|
||||
error,
|
||||
isLoading,
|
||||
} = useGetTasksQuery({ projectId: Number(id) });
|
||||
const { data: tasks, error, isLoading } = useGetTasksQuery({ projectId });
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error || !tasks) return <div>An error occurred while fetching tasks</div>;
|
||||
@@ -123,6 +119,7 @@ const TableView = ({ id, setIsModalNewTaskOpen }: Props) => {
|
||||
<DataGrid
|
||||
rows={tasks || []}
|
||||
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"
|
||||
sx={dataGridSxStyles(isDarkMode)}
|
||||
/>
|
||||
|
||||
@@ -5,19 +5,15 @@ import "gantt-task-react/dist/index.css";
|
||||
import React, { useMemo, useState } from "react";
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
setIsModalNewTaskOpen: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
type TaskTypeItems = "task" | "milestone" | "project";
|
||||
|
||||
const Timeline = ({ id, setIsModalNewTaskOpen }: Props) => {
|
||||
const Timeline = ({ projectId, setIsModalNewTaskOpen }: Props) => {
|
||||
const isDarkMode = useAppSelector((state) => state.global.isDarkMode);
|
||||
const {
|
||||
data: tasks,
|
||||
error,
|
||||
isLoading,
|
||||
} = useGetTasksQuery({ projectId: Number(id) });
|
||||
const { data: tasks, error, isLoading } = useGetTasksQuery({ projectId });
|
||||
|
||||
const [displayOptions, setDisplayOptions] = useState<DisplayOption>({
|
||||
viewMode: ViewMode.Month,
|
||||
@@ -30,7 +26,7 @@ const Timeline = ({ id, setIsModalNewTaskOpen }: Props) => {
|
||||
start: new Date(task.startDate as string),
|
||||
end: new Date(task.dueDate as string),
|
||||
name: task.title,
|
||||
id: `Task-${task.id}`,
|
||||
id: `Task-${task.taskId}`,
|
||||
type: "task" as TaskTypeItems,
|
||||
progress: task.points ? (task.points / 10) * 100 : 0,
|
||||
isDisabled: false,
|
||||
|
||||
@@ -26,16 +26,19 @@ const Project = ({ params }: Props) => {
|
||||
/>
|
||||
<ProjectHeader activeTab={activeTab} setActiveTab={setActiveTab} />
|
||||
{activeTab === "Board" && (
|
||||
<Board id={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
|
||||
<Board projectId={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
|
||||
)}
|
||||
{activeTab === "List" && (
|
||||
<List id={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
|
||||
<List projectId={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
|
||||
)}
|
||||
{activeTab === "Timeline" && (
|
||||
<Timeline id={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
|
||||
<Timeline
|
||||
projectId={id}
|
||||
setIsModalNewTaskOpen={setIsModalNewTaskOpen}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "Table" && (
|
||||
<Table id={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
|
||||
<Table projectId={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -49,14 +49,14 @@ const Search = () => {
|
||||
<h2>Tasks</h2>
|
||||
)}
|
||||
{searchResults.tasks?.map((task) => (
|
||||
<TaskCard key={task.id} task={task} />
|
||||
<TaskCard key={task.taskId} task={task} />
|
||||
))}
|
||||
|
||||
{searchResults.projects && searchResults.projects?.length > 0 && (
|
||||
<h2>Projects</h2>
|
||||
)}
|
||||
{searchResults.projects?.map((project) => (
|
||||
<ProjectCard key={project.id} project={project} />
|
||||
<ProjectCard key={project.projectId} project={project} />
|
||||
))}
|
||||
|
||||
{searchResults.users && searchResults.users?.length > 0 && (
|
||||
|
||||
@@ -20,7 +20,7 @@ const CustomToolbar = () => (
|
||||
);
|
||||
|
||||
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: "productOwnerUsername", headerName: "Product Owner", width: 200 },
|
||||
{
|
||||
@@ -48,6 +48,7 @@ const Teams = () => {
|
||||
slots={{
|
||||
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"
|
||||
sx={dataGridSxStyles(isDarkMode)}
|
||||
/>
|
||||
|
||||
@@ -24,7 +24,7 @@ const Timeline = () => {
|
||||
start: new Date(project.startDate as string),
|
||||
end: new Date(project.endDate as string),
|
||||
name: project.name,
|
||||
id: `Project-${project.id}`,
|
||||
id: `Project-${project.projectId}`,
|
||||
type: "project" as TaskTypeItems,
|
||||
progress: 50,
|
||||
isDisabled: false,
|
||||
|
||||
@@ -31,7 +31,7 @@ const columns: GridColDef[] = [
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="h-9 w-9">
|
||||
<Image
|
||||
src={`/${params.value}`}
|
||||
src={`${process.env.S3_PUBLIC_IMAGE_URL}/${params.value}`}
|
||||
alt={params.row.username}
|
||||
width={100}
|
||||
height={50}
|
||||
|
||||
@@ -80,9 +80,9 @@ export const api = createApi({
|
||||
baseUrl: process.env.NEXT_PUBLIC_API_BASE_URL,
|
||||
prepareHeaders: async (headers) => {
|
||||
const session = await fetchAuthSession();
|
||||
const { accessToken } = session.tokens ?? {};
|
||||
if (accessToken) {
|
||||
headers.set("Authorization", `Bearer ${accessToken}`);
|
||||
const { idToken } = session.tokens ?? {};
|
||||
if (idToken) {
|
||||
headers.set("Authorization", `Bearer ${idToken}`);
|
||||
}
|
||||
return headers;
|
||||
},
|
||||
@@ -120,7 +120,7 @@ export const api = createApi({
|
||||
}),
|
||||
invalidatesTags: ["Projects"],
|
||||
}),
|
||||
getTasks: build.query<Task[], { projectId: number }>({
|
||||
getTasks: build.query<Task[], { projectId: string }>({
|
||||
query: ({ projectId }) => `tasks?projectId=${projectId}`,
|
||||
providesTags: (result) =>
|
||||
result
|
||||
|
||||
1340
tasker-server/layers/nodejs/package-lock.json
generated
Normal file
1340
tasker-server/layers/nodejs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
tasker-server/layers/nodejs/package.json
Normal file
18
tasker-server/layers/nodejs/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1918
tasker-server/package-lock.json
generated
1918
tasker-server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,18 +5,27 @@
|
||||
"scripts": {
|
||||
"infra:init": "cd terraform && AWS_PROFILE=default terraform init",
|
||||
"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": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"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": {
|
||||
"@types/node": "^22.9.1",
|
||||
"eslint": "^9.15.0",
|
||||
"prettier": "^3.3.3",
|
||||
"serverless-prune-plugin": "^2.1.0",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
1181
tasker-server/seed/populateSeedData.ts
Normal file
1181
tasker-server/seed/populateSeedData.ts
Normal file
File diff suppressed because it is too large
Load Diff
179
tasker-server/serverless.yml
Normal file
179
tasker-server/serverless.yml
Normal 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}
|
||||
51
tasker-server/src/handlers/createProject.ts
Normal file
51
tasker-server/src/handlers/createProject.ts
Normal 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}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
69
tasker-server/src/handlers/createTask.ts
Normal file
69
tasker-server/src/handlers/createTask.ts
Normal 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}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
42
tasker-server/src/handlers/createUser.ts
Normal file
42
tasker-server/src/handlers/createUser.ts
Normal 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;
|
||||
};
|
||||
42
tasker-server/src/handlers/getProjects.ts
Normal file
42
tasker-server/src/handlers/getProjects.ts
Normal 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}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
75
tasker-server/src/handlers/getTasks.ts
Normal file
75
tasker-server/src/handlers/getTasks.ts
Normal 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}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
62
tasker-server/src/handlers/getTeams.ts
Normal file
62
tasker-server/src/handlers/getTeams.ts
Normal 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}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
44
tasker-server/src/handlers/getUser.ts
Normal file
44
tasker-server/src/handlers/getUser.ts
Normal 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}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
40
tasker-server/src/handlers/getUserTasks.ts
Normal file
40
tasker-server/src/handlers/getUserTasks.ts
Normal 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}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
42
tasker-server/src/handlers/getUsers.ts
Normal file
42
tasker-server/src/handlers/getUsers.ts
Normal 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}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
52
tasker-server/src/handlers/updateTaskStatus.ts
Normal file
52
tasker-server/src/handlers/updateTaskStatus.ts
Normal 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}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
97
tasker-server/src/lib/util.ts
Normal file
97
tasker-server/src/lib/util.ts
Normal 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 ?? [];
|
||||
};
|
||||
@@ -76,6 +76,20 @@ resource "aws_ssm_parameter" "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" {
|
||||
name = "/tasker/cognito/client-id"
|
||||
description = "Tasker Cognito Client ID"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
resource "aws_dynamodb_table" "tasker_project_table" {
|
||||
name = "tasker-project-table"
|
||||
billing_mode = "PAY_PER_REQUEST"
|
||||
hash_key = "type"
|
||||
hash_key = "category"
|
||||
range_key = "projectId"
|
||||
|
||||
attribute {
|
||||
name = "type"
|
||||
name = "category"
|
||||
type = "S"
|
||||
}
|
||||
|
||||
@@ -30,11 +30,11 @@ resource "aws_ssm_parameter" "tasker_project_table_arn" {
|
||||
resource "aws_dynamodb_table" "tasker_user_table" {
|
||||
name = "tasker-user-table"
|
||||
billing_mode = "PAY_PER_REQUEST"
|
||||
hash_key = "type"
|
||||
hash_key = "category"
|
||||
range_key = "cognitoId"
|
||||
|
||||
attribute {
|
||||
name = "type"
|
||||
name = "category"
|
||||
type = "S"
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ resource "aws_dynamodb_table" "tasker_user_table" {
|
||||
|
||||
global_secondary_index {
|
||||
name = "GSI-user-id"
|
||||
hash_key = "type"
|
||||
hash_key = "category"
|
||||
range_key = "userId"
|
||||
projection_type = "ALL"
|
||||
}
|
||||
@@ -71,11 +71,11 @@ resource "aws_ssm_parameter" "tasker_user_table_arn" {
|
||||
resource "aws_dynamodb_table" "tasker_team_table" {
|
||||
name = "tasker-team-table"
|
||||
billing_mode = "PAY_PER_REQUEST"
|
||||
hash_key = "type"
|
||||
hash_key = "category"
|
||||
range_key = "teamId"
|
||||
|
||||
attribute {
|
||||
name = "type"
|
||||
name = "category"
|
||||
type = "S"
|
||||
}
|
||||
|
||||
@@ -100,11 +100,11 @@ resource "aws_ssm_parameter" "tasker_team_table_arn" {
|
||||
resource "aws_dynamodb_table" "tasker_task_table" {
|
||||
name = "tasker-task-table"
|
||||
billing_mode = "PAY_PER_REQUEST"
|
||||
hash_key = "type"
|
||||
hash_key = "category"
|
||||
range_key = "taskId"
|
||||
|
||||
attribute {
|
||||
name = "type"
|
||||
name = "category"
|
||||
type = "S"
|
||||
}
|
||||
|
||||
@@ -130,21 +130,21 @@ resource "aws_dynamodb_table" "tasker_task_table" {
|
||||
|
||||
global_secondary_index {
|
||||
name = "GSI-project-id"
|
||||
hash_key = "type"
|
||||
hash_key = "category"
|
||||
range_key = "projectId"
|
||||
projection_type = "ALL"
|
||||
}
|
||||
|
||||
global_secondary_index {
|
||||
name = "GSI-author-user-id"
|
||||
hash_key = "type"
|
||||
hash_key = "category"
|
||||
range_key = "authorUserId"
|
||||
projection_type = "ALL"
|
||||
}
|
||||
|
||||
global_secondary_index {
|
||||
name = "GSI-assigned-user-id"
|
||||
hash_key = "type"
|
||||
hash_key = "category"
|
||||
range_key = "assignedUserId"
|
||||
projection_type = "ALL"
|
||||
}
|
||||
@@ -165,11 +165,11 @@ resource "aws_ssm_parameter" "tasker_task_table_arn" {
|
||||
resource "aws_dynamodb_table" "tasker_task_extra_table" {
|
||||
name = "tasker-task-extra-table"
|
||||
billing_mode = "PAY_PER_REQUEST"
|
||||
hash_key = "type"
|
||||
hash_key = "category"
|
||||
range_key = "id"
|
||||
|
||||
attribute {
|
||||
name = "type"
|
||||
name = "category"
|
||||
type = "S"
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ resource "aws_dynamodb_table" "tasker_task_extra_table" {
|
||||
|
||||
global_secondary_index {
|
||||
name = "GSI-task-id"
|
||||
hash_key = "type"
|
||||
hash_key = "category"
|
||||
range_key = "taskId"
|
||||
projection_type = "ALL"
|
||||
}
|
||||
|
||||
24
tasker-server/terraform/layer.tf
Normal file
24
tasker-server/terraform/layer.tf
Normal 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
|
||||
}
|
||||
@@ -1,11 +1,22 @@
|
||||
resource "aws_s3_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" {
|
||||
bucket = aws_s3_bucket.tasker_public_images.id
|
||||
|
||||
Reference in New Issue
Block a user