Purpose
This project was completed as part of a group learning exercise. It focused on implementing an RLS based approach to auth provided through supabase.
Inspired by the https://devjam.vercel.app/ site an attempt was made to add the ability to vote on projects.
Demo
https://modest-newton-235fe0.netlify.app/
Project Features
Directly from the project description:
Allow users to vote give multiple choices
This project allows a group of people to decide on coding projects to build as a group. A deadline is set for each project. At the end of a submission phase, users can vote for different awards based on the submitted projects.
After the voting phase ends, the awards are allocated to the users with the most votes.
User Stories
- User must enter their name to take part in a poll
In this project we do not require an account to view the projects but a discord login is required to submit or vote.
- User is shown multiple polls that they can choose from
Each project is a poll, the submitted projects can be voted on.
- After clicking on a poll, user is shown a voting screen where they can vote on that poll
When a user chooses a project that is in the voting phase, they can choose from a list of awards. Users can't vote for themselves and they cannot vote the same award for two submissions.
- Store items and votes in a database
It's all in Postgres hosted by supabase.
- After voting, the user should see the results from everyone
When the voting phase is complete the the results are publicly visible for each project.
- User can only vote once per poll
Well, they can vote multiple times but only once per award per poll.
Bonus features
- Only allow authenticated users to vote
- User can create their own polls
Only admins can create polls in the current implementation. Authenticated users can submit projects and vote. Anonymous users can only view project and submissions.
Technical Specifications
Decided to use react-query
for cache this time which is not normalized. We are storing the users, projects, awards, submissions and votes in a PostgreSQL relational database. The API uses PostgREST via supabase.
export const fetchProjects = () =>
supabase.from<IProjectResource>("projects").select();
Which is a query built to retrieve the list of projects
on the server. Where an IProjectResource
has the following interface:
interface IProjectResource {
id: string;
created_at: string;
updated_at: string;
started_at: string;
ended_at: string;
completed_at: string;
archived_at: string;
title: string;
summary: string;
complexity: string;
description: string;
}
We can call fetchProjects
within a custom hook. This makes it easier to manage the cache. All projects retrieved with this hook can share the cache key of projects
.
export const useProjects = () => {
const query = useQuery<IProjectResource[], Error>(
"projects",
fetchProjects as any
);
return query.data || [];
};
With this data the layout can be provided for the user to choose a project. When a project is selected, similar code is used to fetch the submissions and awards.
The important timestamps are related to the scheduling of the various phases of the project.
started_at
: Start allowing submissions on this date.ended_at
: Stop allowing submissions, and start allowing votes on this date.completed_at
: Stop allowing votes on this date.
The actions that are allowed to be taken by the users for each project is determined by the current date in relation to these values. This is implemented using RLS.
The submissions
table is RLS enabled. Users can not update or delete submissions. Their ability to select
submissions is determined by the current date. We can only insert
submissions in between the started_at
and ended_at
dates.
src/services/schema/tables.sql:
create policy "Submissions inserted: project is active, authenticated user"
on submissions for insert with check (
auth.role() = 'authenticated'
and
get_is_submission_accepted(submissions.project_id)
and
get_is_submission_first(submissions.project_id, submissions.user_id)
);
This RLS policy only allows inserts
when all of the following conditions are met:
- The user making the request is
authenticated
, this ensures they are logged in using discord and the submission will be tied to a username. - The current date is in between the
started_at
andended_at
dates of the project. - The submission is the first from this user.
The utility function for second constraint is also defined.
src/services/schema/tables.sql:
create or replace function get_is_submission_accepted(_project_id uuid)
returns boolean
language sql
security definer
set search_path = public
as $$
select exists(
select id
from projects
where id = _project_id
and timezone('utc'::text, now())
between projects.started_at and projects.ended_at
);
$$;
Since these operations are limited by RLS we can hide any UI that allows the user to add submissions. This techique is used througout the React pages.
Dependencies
- react@next
- react-router-dom@6
- supabase/js
- react-query
- tailwindcss
- postgres
Great to have the release for React Router 6 for this. I feel like the API has improved, makes it easier to compose routes.
Creating more composable styles from tailwind hasn't been a focus, too much duplicate JSX in the components currently.
The security policies should be tested using something more like https://github.com/steve-chavez/socnet/tree/master/tests.
Auth
An auth API is provided by supabase. The support for providers is growing consistently. We see that TikTok is added around the time of this README commit.
The only provider enabled for this project is discord. The API to sign in can be found in src/context/auth.tsx.
supabase.auth.signIn({ provider: "discord" });
This is not called directly. It is wrapped in a login
function. Users can login by clicking on the src/components/LoginButton.tsx. When they complete the OAuth redirect the session will be available using the AuthContext
.
<AuthContext.Provider
value={{
user: session?.user ?? null,
login,
logout,
}}
>
{props.children}
</AuthContext.Provider>
We can verify that a user is authenticated before allowing them to view pages by combinding the AuthContext
with React Router.
export const VerifyAuthenticated = ({
children,
}: {
children: JSX.Element;
}) => {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return <Navigate to="/login" state={{ from: location }} />;
}
return children;
};
Responsive
Incomplete
- Not all the RLS is complete, there also needs to be tests for the security configuration
- Styling and tailwind stuff is messy
- Need to write some tests and refactor the component tree