feat: add cra-todo-list-auth in examples

This commit is contained in:
YashKandalkar committed 2021-01-15 20:28:32 +05:30
1 parent 67cb034987
commit 210829fe4e
30 files changed
+12529

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.eslintcache
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/../../../../../:\supabase\examples\cra-todo-list-auth\.idea/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
</component>
</project>
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/cra-todo-list-auth.iml" filepath="$PROJECT_DIR$/.idea/cra-todo-list-auth.iml" />
</modules>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myRunOnSave" value="true" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component>
</project>
+5
View File
@@ -0,0 +1,5 @@
{
"tabWidth": 4,
"semi": true,
"printWidth": 80
}
+81
View File
@@ -0,0 +1,81 @@
# Todo example using Supabase
- Frontend:
- [create-react-app](https://reactjs.org/docs/create-a-new-react-app.html). React Framework for building single-page applications.
- [Tailwind](https://tailwindcss.com/) for styling and layout.
- [Supabase.js](https://supabase.io/docs/library/getting-started) for user management and realtime data syncing.
- Backend:
- [app.supabase.io/](https://app.supabase.io/): hosted Postgres database with restful API for usage with Supabase.js.
## Demo
-not added-
## Deploy with Vercel
-not added-
## Build from scratch
### 1. Create new project
Sign up to Supabase - [https://app.supabase.io](https://app.supabase.io) and create a new project. Wait for your database to start.
### 2. Run "Todo List" Quickstart
Once your database has started, run the "Todo List" quickstart.
### 3. Get the URL and Key
Go to the Project Settings (the cog icon), open the API tab, and find your API URL and `anon` key, you'll need these in the next step.
The `anon` key is your client-side API key. It allows "anonymous access" to your database, until the user has logged in. Once they have logged in, the keys will switch to the user's own login token. This enables row level security for your data. Read more about this [below](#postgres-row-level-security).
![image](https://user-images.githubusercontent.com/10214025/88916245-528c2680-d298-11ea-8a71-708f93e1ce4f.png)
**_NOTE_**: The `service_role` key has full access to your data, bypassing any security policies. These keys have to be kept secret and are meant to be used in server environments and never on a client or browser.
You will be asked for a `REACT_APP_SUPABASE_URL` and `REACT_APP_SUPABASE_KEY`. Use the API URL and `anon` key from [step 3](#3.-get-the-url-and-key).
## Supabase details
### Postgres Row level security
This project uses very high-level Authorization using Postgres' Role Level Security.
When you start a Postgres database on Supabase, we populate it with an `auth` schema, and some helper functions.
When a user logs in, they are issued a JWT with the role `authenticated` and thier UUID.
We can use these details to provide fine-grained control over what each user can and cannot do.
This is a trimmed-down schema, with the policies:
```sql
create table todos (
id bigint generated by default as identity primary key,
user_id uuid references auth.users not null,
task text check (char_length(task) > 3),
is_complete boolean default false,
inserted_at timestamp with time zone default timezone('utc'::text, now()) not null
);
alter table todos enable row level security;
create policy "Individuals can create todos." on todos for
insert with check (auth.uid() = user_id);
create policy "Individuals can view their own todos. " on todos for
select using (auth.uid() = user_id);
create policy "Individuals can update their own todos." on todos for
update using (auth.uid() = user_id);
create policy "Individuals can delete their own todos." on todos for
delete using (auth.uid() = user_id);
```
## Authors
- [Supabase](https://supabase.io)
Supabase is open source, we'd love for you to follow along and get involved at https://github.com/supabase/supabase
@@ -0,0 +1,10 @@
module.exports = {
style: {
postcss: {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
],
},
},
}
+44
View File
@@ -0,0 +1,44 @@
{
"name": "cra-todo-list-auth",
"version": "0.1.0",
"private": true,
"dependencies": {
"@craco/craco": "^6.0.0",
"@supabase/supabase-js": "^1.2.1",
"@tailwindcss/postcss7-compat": "^2.0.2",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"autoprefixer": "9",
"postcss": "7",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-scripts": "4.0.1",
"tailwindcss": "npm:@tailwindcss/postcss7-compat",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="CRA Supabase TodoList Example"
/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Supabase TodoList Example</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
@@ -0,0 +1,15 @@
{
"short_name": "CRA Supabase TodoList Example",
"name": "Create React App Supabase TodoList Example",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
+32
View File
@@ -0,0 +1,32 @@
import { useState, useEffect } from "react";
import { supabase } from "./lib/api";
import Auth from "./components/Auth";
import Home from "./components/Home";
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
const session = supabase.auth.session();
setUser(session?.user ?? null);
const { data: authListener } = supabase.auth.onAuthStateChange(
async (event, session) => {
const currentUser = session?.user;
setUser(currentUser ?? null);
}
);
return () => {
authListener?.unsubscribe();
};
}, [user]);
return (
<div className="min-w-full min-h-screen flex items-center justify-center bg-gray-200">
{!user ? <Auth /> : <Home user={user} />}
</div>
);
}
export default App;
@@ -0,0 +1,181 @@
import { useRef, useState } from "react";
import { supabase } from "../lib/api";
const Auth = () => {
const [helperText, setHelperText] = useState({ error: null, text: null });
const emailRef = useRef();
const passwordRef = useRef();
const handleLogin = async (type) => {
const email = emailRef.current?.value;
const password = passwordRef.current?.value;
const { user, error } =
type === "LOGIN"
? await supabase.auth.signIn({ email, password })
: await supabase.auth.signUp({ email, password });
if (error) {
setHelperText({ error: true, text: error.message });
} else if (!user && !error) {
setHelperText({
error: false,
text: "An email has been sent to you for verification!",
});
}
};
const handleOAuthLogin = async (provider) => {
// You need to enable the third party auth you want in Authentication > Settings
// Read more on: https://supabase.io/docs/guides/auth#third-party-logins
let { error } = await supabase.auth.signIn({ provider });
if (error) console.log("Error: ", error.message);
};
const forgotPassword = async (e) => {
// Read more on https://supabase.io/docs/client/reset-password-email#notes
e.preventDefault();
const email = prompt("Please enter your email:");
if (email === null || email === "") {
setHelperText({ error: true, text: "You must enter your email." });
} else {
let { error } = await supabase.auth.api.resetPasswordForEmail(
email
);
if (error) {
console.error("Error: ", error.message);
} else {
setHelperText({
error: false,
text: "Password recovery email has been sent.",
});
}
}
};
return (
<div
className={
"w-full h-full sm:h-auto sm:w-2/5 max-w-sm p-5 bg-white shadow flex flex-col text-base"
}
>
<span
className={
"font-sans text-4xl text-center pb-2 mb-1 border-b mx-4 align-center"
}
>
Login
</span>
<label
className={"mt-3 mb-2 font-medium text-lg"}
htmlFor={"email"}
>
<span className={"font-mono mr-1 text-red-400"}>*</span>Email:
</label>
<input
className={"bg-gray-100 border py-1 px-3"}
type={"email"}
name={"email"}
ref={emailRef}
required
/>
<label
className={"mt-3 mb-2 font-medium text-lg"}
htmlFor={"password"}
>
<span className={"font-mono mr-1 text-red-400"}>*</span>
Password:
</label>
<input
className={"bg-gray-100 border py-1 px-3"}
type={"password"}
name={"password"}
ref={passwordRef}
required
/>
<span
className={
"text-blue-600 mt-2 cursor-pointer self-end text-sm font-medium"
}
onClick={forgotPassword}
>
Forgot Password?
</span>
{!!helperText.text && (
<div
className={`border px-1 py-2 my-2 text-center text-sm ${
helperText.error
? "bg-red-100 border-red-300 text-red-400"
: "bg-green-100 border-green-300 text-green-500"
}`}
>
{helperText.text}
</div>
)}
<div className="mt-2 flex">
<span className="block mx-1.5 w-full rounded-md shadow-sm">
<button
type="submit"
onClick={() =>
handleLogin("REGISTER").catch(console.error)
}
className={
"border w-full border-blue-600 text-blue-600 flex justify-center py-2 px-4 text-sm font-medium rounded-md hover:bg-blue-200 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition duration-150 ease-in-out"
}
>
Sign Up
</button>
</span>
<span className="block w-full mx-1.5 rounded-md shadow-sm">
<button
onClick={() => handleLogin("LOGIN")}
type="button"
className="flex w-full justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition duration-150 ease-in-out"
>
Sign In
</button>
</span>
</div>
<div className="mt-3">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full mx-1.5 border-t border-gray-300" />
</div>
<div className="relative flex justify-center text-sm leading-5">
<span className="px-2 bg-white text-gray-500">
Or continue with
</span>
</div>
</div>
<div>
<div className="mt-3">
<span className="block rounded-md shadow-sm">
<button
onClick={() => handleOAuthLogin("github")}
type="button"
className="w-3/4 mx-auto flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition duration-150 ease-in-out"
>
GitHub
</button>
</span>
</div>
<div className="mt-3">
<span className="block rounded-md shadow-sm">
<button
onClick={() => handleOAuthLogin("google")}
type="button"
className="w-3/4 mx-auto flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition duration-150 ease-in-out"
>
Google
</button>
</span>
</div>
</div>
</div>
</div>
);
};
export default Auth;
@@ -0,0 +1,161 @@
import { useEffect, useState, useRef } from "react";
import { supabase } from "../lib/api";
import RecoverPassword from "./RecoverPassword";
import TodoItem from "./TodoItem";
const Home = ({ user }) => {
const [recoveryToken, setRecoveryToken] = useState(null);
const [todos, setTodos] = useState([]);
const newTaskTextRef = useRef();
const [errorText, setError] = useState("");
useEffect(() => {
/* Recovery url is of the form
* <SITE_URL>#access_token=x&refresh_token=y&expires_in=z&token_type=bearer&type=recovery
* Read more on https://supabase.io/docs/client/reset-password-email#notes
*/
let url = window.location.hash;
let query = url.substr(1);
let result = {};
query.split("&").forEach((part) => {
const item = part.split("=");
result[item[0]] = decodeURIComponent(item[1]);
});
if (result.type === "recovery") {
setRecoveryToken(result.access_token);
}
fetchTodos().catch(console.error);
}, []);
const fetchTodos = async () => {
let { data: todos, error } = await supabase
.from("todos")
.select("*")
.order("id", { ascending: false });
if (error) console.log("error", error);
else setTodos(todos);
};
const deleteTodo = async (id) => {
try {
await supabase.from("todos").delete().eq("id", id);
setTodos(todos.filter((x) => x.id !== id));
} catch (error) {
console.log("error", error);
}
};
const addTodo = async () => {
let taskText = newTaskTextRef.current.value;
let task = taskText.trim();
if (task.length <= 3) {
setError("Task length should be more than 3!");
} else {
let { data: todo, error } = await supabase
.from("todos")
.insert({ task, user_id: user.id })
.single();
if (error) setError(error.message);
else {
setTodos([todo, ...todos]);
setError(null);
newTaskTextRef.current.value = "";
}
}
};
const handleLogout = async () => {
supabase.auth.signOut().catch(console.error);
};
return recoveryToken ? (
<RecoverPassword
token={recoveryToken}
setRecoveryToken={setRecoveryToken}
/>
) : (
<div className={"w-screen fixed flex flex-col min-h-screen bg-gray-50"}>
<header
className={
"flex justify-between items-center px-4 h-16 bg-gray-900"
}
>
<span
className={
"text-2xl sm:text-4xl text-white border-b font-sans"
}
>
Todo List.
</span>
<button
onClick={handleLogout}
className={
"flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition duration-150 ease-in-out"
}
>
Logout
</button>
</header>
<div
className={"flex flex-col flex-grow p-4"}
style={{ height: "calc(100vh - 11.5rem)" }}
>
<div
className={`p-2 border flex-grow grid gap-2 ${
todos.length ? "auto-rows-min" : ""
} grid-cols-1 h-2/3 overflow-y-scroll first:mt-8`}
>
{todos.length ? (
todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onDelete={() => deleteTodo(todo.id)}
/>
))
) : (
<span
className={
"h-full flex justify-center items-center"
}
>
You do have any tasks yet!
</span>
)}
</div>
{!!errorText && (
<div
className={
"border max-w-sm self-center px-4 py-2 mt-4 text-center text-sm bg-red-100 border-red-300 text-red-400"
}
>
{errorText}
</div>
)}
</div>
<div className={"flex m-4 mt-0 h-10"}>
<input
ref={newTaskTextRef}
type="text"
onKeyUp={(e) => e.key === "Enter" && addTodo()}
className={
"bg-gray-200 border px-2 border-gray-300 w-full mr-4"
}
/>
<button
onClick={addTodo}
className={
"flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition duration-150 ease-in-out"
}
>
Add
</button>
</div>
</div>
);
};
export default Home;
@@ -0,0 +1,57 @@
import { supabase } from "../lib/api";
import { useRef } from "react";
const RecoverPassword = ({ token, setRecoveryToken }) => {
const newPasswordRef = useRef();
const handleNewPassword = async () => {
const newPassword = newPasswordRef.current.value;
const { error } = await supabase.auth.api.updateUser(token, {
password: newPassword,
});
if (!error) {
// To render our Todo list again
setRecoveryToken(null);
} else {
console.error(error);
}
};
return (
<div
className={
"w-full h-full sm:h-auto sm:w-2/5 max-w-sm p-5 bg-white shadow flex flex-col text-base"
}
>
<span
className={
"font-sans text-2xl text-center pb-2 mb-1 border-b mx-4 align-center"
}
>
Recover Password
</span>
<label className={"mt-3 mb-2 text-lg"} htmlFor={"email"}>
<span className={"font-mono mr-1 text-red-400"}>*</span>Enter
new password:
</label>
<input
className={"bg-gray-100 border py-1 px-3"}
type="password"
ref={newPasswordRef}
required
/>
<span className="block mt-4 rounded-md shadow-sm">
<button
onClick={handleNewPassword}
type="button"
className="flex mx-auto justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition duration-150 ease-in-out"
>
Change Password
</button>
</span>
</div>
);
};
export default RecoverPassword;
@@ -0,0 +1,52 @@
import { useState } from "react";
import { supabase } from "../lib/api";
const TodoItem = ({ todo, onDelete }) => {
const [isCompleted, setIsCompleted] = useState(todo.is_complete);
const toggleCompleted = async () => {
const { data, error } = await supabase
.from("todos")
.update({ is_complete: !isCompleted })
.eq("id", todo.id)
.single();
if (error) {
console.error(error);
}
setIsCompleted(data.is_complete);
};
return (
<div
className={"p-3 max-h-14 flex align-center justify-between border"}
>
<span className={"truncate flex-grow"}>
<input
className="cursor-pointer mr-2"
onChange={toggleCompleted}
type="checkbox"
checked={isCompleted ? true : ""}
/>
<span
className={`w-full flex-grow ${
isCompleted ? "line-through" : ""
}`}
>
{todo.task}
</span>
</span>
<button
className={"font-mono text-red-500 text-xl border px-2"}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onDelete();
}}
>
X
</button>
</div>
);
};
export default TodoItem;
+13
View File
@@ -0,0 +1,13 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
+17
View File
@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
@@ -0,0 +1,5 @@
import { createClient } from '@supabase/supabase-js';
import { REACT_APP_SUPABASE_URL, REACT_APP_SUPABASE_KEY } from './constants';
export const supabase = createClient(REACT_APP_SUPABASE_URL, REACT_APP_SUPABASE_KEY);
@@ -0,0 +1,2 @@
export const REACT_APP_SUPABASE_URL=process.env.REACT_APP_SUPABASE_URL
export const REACT_APP_SUPABASE_KEY=process.env.REACT_APP_SUPABASE_KEY
@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
@@ -0,0 +1,11 @@
module.exports = {
purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
File diff suppressed because it is too large. Load diff