diff --git a/package-lock.json b/package-lock.json index 7ec5173..e8a51f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,10 @@ "@radix-ui/react-slot": "^1.1.0", "@reduxjs/toolkit": "^2.2.6", "@supabase/ssr": "^0.4.0", + "axios": "^1.7.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "ldrs": "^1.0.2", "lucide-react": "^0.408.0", "next": "14.2.5", "next-client-cookies": "^1.1.1", @@ -1222,6 +1224,12 @@ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.19", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", @@ -1260,6 +1268,17 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -1536,6 +1555,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1634,6 +1665,15 @@ } } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1761,6 +1801,26 @@ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", "license": "MIT" }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", @@ -1777,6 +1837,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -2064,6 +2138,12 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, + "node_modules/ldrs": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ldrs/-/ldrs-1.0.2.tgz", + "integrity": "sha512-sYJmivdkIiHrUEqTrEWccBoLdaENpzbzkABI5rk8rRxTXrg9i2xVuDvUUuhOhJY3RmQyaoxs046pM1DCRdcIpg==", + "license": "MIT" + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -2128,6 +2208,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -2608,6 +2709,12 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/package.json b/package.json index ed5222b..ec8026f 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,10 @@ "@radix-ui/react-slot": "^1.1.0", "@reduxjs/toolkit": "^2.2.6", "@supabase/ssr": "^0.4.0", + "axios": "^1.7.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "ldrs": "^1.0.2", "lucide-react": "^0.408.0", "next": "14.2.5", "next-client-cookies": "^1.1.1", diff --git a/src/app/api/logout/route.ts b/src/app/api/logout/route.ts new file mode 100644 index 0000000..0970273 --- /dev/null +++ b/src/app/api/logout/route.ts @@ -0,0 +1,10 @@ +import {NextRequest, NextResponse} from "next/server"; +import {createClient} from "@/utils/supabase/server"; + +export async function GET(req: NextRequest) { + const supabase = createClient() + + const { error } = await supabase.auth.signOut() + + return NextResponse.redirect(new URL("/form_create", req.url)) +} \ No newline at end of file diff --git a/src/app/api/rest/v1/isUsername/route.ts b/src/app/api/rest/v1/isUsername/route.ts new file mode 100644 index 0000000..8679bc0 --- /dev/null +++ b/src/app/api/rest/v1/isUsername/route.ts @@ -0,0 +1,29 @@ +import {NextRequest, NextResponse} from "next/server"; + +export async function POST(req: NextRequest) { + let response = NextResponse.next({ + request: { + headers: req.headers, + }, + }) + + const body = await req.json() + + if ( !body.username ) { + return NextResponse.json({ error: 'username is required' }) + } + + let res = await fetch(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/rpc/is_username_exist`, { + method: 'POST', + headers: { + "apikey": process.env.SERVICE_KEY!, + "Authorization": `Bearer ${process.env.SERVICE_KEY!}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body) + }); + + let data = await res.json(); + + return NextResponse.json({ state: data }); +} \ No newline at end of file diff --git a/src/app/api/rest/v1/users/route.ts b/src/app/api/rest/v1/users/route.ts new file mode 100644 index 0000000..c61b750 --- /dev/null +++ b/src/app/api/rest/v1/users/route.ts @@ -0,0 +1,39 @@ +import {NextRequest, NextResponse} from "next/server"; +import { createClient } from "@/utils/supabase/server"; +import { User } from "@supabase/supabase-js"; + +export async function POST(req: NextRequest) { + let response = NextResponse.next({ + request: { + headers: req.headers, + }, + }) + + const body = await req.json() + const supabase = createClient(); + const searchParams = req.nextUrl.searchParams; + const option = searchParams.get('option'); + + let data: User | User[] | null = null; + + if ( option === 'insert' ) { + const { data: user, error: error } = await supabase + .from('users') + .insert([{id: body.id, username: body.username, admin: body.admin}]) + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + data = user; + } else if ( option === 'update' ) { + const { data: user, error: error } = await supabase + .from('users') + .update({username: body.username}) + .match({id: body.id}) + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + data = user; + } + + return NextResponse.json({ data: data }); +} \ No newline at end of file diff --git a/src/app/auth/action.tsx b/src/app/auth/action.tsx index 601847b..e06c965 100644 --- a/src/app/auth/action.tsx +++ b/src/app/auth/action.tsx @@ -21,51 +21,53 @@ export const Login = async ( export const SignUp = async ( credentials : { + username: string | null, email: string , password: string , }) => { const origin = headers().get("origin"); - const referer = headers().get("referer"); - const query = referer?.split('?')[1].split('&'); - const org = query?.find((q) => q.includes('organisation')) || ''; - + const username = credentials.username; const supabase = createClient(); - const { data: { session }, error, } = await supabase.auth.signUp({ + + const { data: { user, session }, error, } = await supabase.auth.signUp({ email: credentials.email as string, password: credentials.password as string, options: { - emailRedirectTo: `${origin}/auth/callback`, + emailRedirectTo: `${origin}/auth/confirm`, + data: { username: username }, }, }); + if (error) { + console.log(error); return { error: error.message }; } - if (session) { + if (session || user?.role !== 'authenticated') { return { error: 'Email already exists' }; } - return { error: null }; + return { user_id: user?.id, error: null }; }; export const AuthSignIn = async () => { - const origin = headers().get("origin"); - const gmail = cookies()?.get('email')?.value || ''; - - const supabase = createClient(); - const { data, error } = await supabase.auth.signInWithOAuth({ - provider: 'google', - options: { - redirectTo: `${origin}/auth/callback`, - queryParams: { - include_granted_scopes: 'true', - access_type: 'offline', - prompt: 'select_account', - login_hint: gmail, - }, - }, - }); - if (error) return { error: error.message, url: null }; - if (data.url) return { error: null, url: data.url }; - return { error: 'Error signing in', url: null }; + const origin = headers().get("origin"); + const gmail = cookies()?.get('email')?.value || ''; + + const supabase = createClient(); + const { data, error } = await supabase.auth.signInWithOAuth({ + provider: 'google', + options: { + redirectTo: `${origin}/auth/callback`, + queryParams: { + include_granted_scopes: 'true', + access_type: 'offline', + prompt: 'select_account', + login_hint: gmail, + }, + }, + }); + if (error) return { error: error.message, url: null }; + if (data.url) return { error: null, url: data.url }; + return { error: 'Error signing in', url: null }; } export async function usernameExisits(username: string): Promise { diff --git a/src/app/auth/callback/route.ts b/src/app/auth/callback/route.ts index 96cfad6..4bb206f 100644 --- a/src/app/auth/callback/route.ts +++ b/src/app/auth/callback/route.ts @@ -23,5 +23,6 @@ export async function GET(req: NextRequest, res: NextResponse) { console.log('x ',error); } } + return NextResponse.redirect(url); } \ No newline at end of file diff --git a/src/app/auth/component/component.tsx b/src/app/auth/component/component.tsx index ac64735..13f9eff 100644 --- a/src/app/auth/component/component.tsx +++ b/src/app/auth/component/component.tsx @@ -1,15 +1,16 @@ -import { FormEvent, use, useEffect, useState } from "react"; +import React, { FormEvent, use, useEffect, useState } from "react"; import { usernameExisits } from "@/app/auth/action"; import { Label } from "@/components/ui/label" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" +import Loader from '@/components/ui/loader' -import { VscEye, VscEyeClosed } from "react-icons/vsc"; -import CircularProgress from '@mui/material/CircularProgress'; +import { VscEye, VscEyeClosed } from "react-icons/vsc" import styles from './styles.module.css' import { cn } from "@/lib/utils" + interface Props { auth: string | null SignUp: (e: EventTarget & HTMLFormElement) => Promise @@ -90,22 +91,21 @@ export function Component( props : Props) { } setLoading(true); const data = await usernameExisits(username) - console.log('data = ',data); + if (data) { - const nextUserSibling = current.username.nextSibling as HTMLElement; + const nextUserSibling = current.username.nextElementSibling as HTMLElement; nextUserSibling.innerText = 'Username already exists'; } else { - const nextUserSibling = current.username.nextSibling as HTMLElement; + const nextUserSibling = current.username.nextElementSibling as HTMLElement; nextUserSibling.innerText = ''; if (validatePassword(password) && password.length>=8) { - const bool = await props.SignUp(current) if (!bool) setLoading(false); - const nextSibling = current.password.nextSibling as HTMLElement; + const nextSibling = current.password.nextElementSibling as HTMLElement; nextSibling.innerText = ''; } else { - const nextSibling = current.password.nextSibling as HTMLElement; + const nextSibling = current.password.nextElementSibling as HTMLElement; if (!validatePassword(password, 1)) { nextSibling.innerText = 'Password must be at least 8 characters long'; } else if (!validatePassword(password, 2)) { @@ -171,7 +171,8 @@ export function Component( props : Props) { className="group relative flex w-full justify-center rounded-[8px] bg-primary py-6 px-4 text-md font-bold text-primary-foreground transition-colors focus:opacity-90 focus:outline-none" disabled={loading} > - {loading ? : structure.button.text} + {loading ? + : structure.button.text} @@ -190,7 +191,7 @@ function validateEmail(email: string): boolean { function validatePassword(password: string, customCheck: number = 0): boolean { const re = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{8,}$/; const special = /[!@#$%^&*()_+]/; - const upper_lower_digit = /[a-z]/ && /[A-Z]/ && /\d/; + const upper_lower_digit = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{8,}$/; if (!customCheck) return re.test(password); if (customCheck === 1) return password.length >= 8; if (customCheck === 2) return upper_lower_digit.test(password); diff --git a/src/app/auth/component/oauth.tsx b/src/app/auth/component/oauth.tsx index 9bc48cb..faba5ee 100644 --- a/src/app/auth/component/oauth.tsx +++ b/src/app/auth/component/oauth.tsx @@ -1,10 +1,9 @@ import React, { useState } from "react"; import { Button } from "@/components/ui/button" +import Loader from '@/components/ui/loader' // -- icons -- -import CircularProgress from '@mui/material/CircularProgress'; import { FcGoogle } from "react-icons/fc"; - import { JSX, SVGProps } from "react" interface Props{ @@ -34,10 +33,8 @@ export function OAuthComponent(props : Props) { variant="outline" className="group relative flex w-full justify-center rounded-[8px] border border-input bg-background py-6 px-4 text-md font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none focus:opacity-90" > - {loading ? : - <> - oogle - } + {loading ? : + <>oogle} ) diff --git a/src/app/auth/confirm/route.ts b/src/app/auth/confirm/route.ts index 55a8b92..b654759 100644 --- a/src/app/auth/confirm/route.ts +++ b/src/app/auth/confirm/route.ts @@ -1,6 +1,5 @@ import { type EmailOtpType } from '@supabase/supabase-js' import { type NextRequest, NextResponse } from 'next/server' -import { cookies } from 'next/headers' import { createClient } from '@/utils/supabase/server' @@ -8,7 +7,7 @@ export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url) const token_hash = searchParams.get('token_hash') const type = searchParams.get('type') as EmailOtpType | null - const next = searchParams.get('next') ?? '/create_form' + const next = searchParams.get('next') ?? '/form_create' const redirectTo = req.nextUrl.clone() redirectTo.pathname = next @@ -21,18 +20,6 @@ export async function GET(req: NextRequest) { type: type || 'email', token_hash: token_hash , }) - const user = data?.user - if (!error && user) { - const getCookies = cookies(); - const username = getCookies?.get('username')?.value; - getCookies?.delete('username'); - const { error: insertError } = await supabase - .from('users') - .insert([ - { id: user?.id, username: username } - ]); - if (insertError) console.error('Error inserting user data:', insertError.message); - } if (!error) { redirectTo.searchParams.delete('next') return NextResponse.redirect(redirectTo) diff --git a/src/app/auth/confirm_email/page.tsx b/src/app/auth/confirm_email/page.tsx index 9928445..00cef0a 100644 --- a/src/app/auth/confirm_email/page.tsx +++ b/src/app/auth/confirm_email/page.tsx @@ -1,9 +1,10 @@ 'use client'; -import React, { useState, useEffect, use } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import Link from 'next/link'; -import { Button } from '@/components/ui/button'; + import { useSearchParams } from 'next/navigation'; import { supabase } from '@/utils/supabase/client'; +import { Button } from '@/components/ui/button'; import Image from 'next/image'; @@ -12,10 +13,22 @@ export default function Confirm() { const [error, setError] = useState(null); const [countdown, setCountdown] = useState(10); + useEffect(() => { + supabase.auth.getUser().then(({ data , error }) => { + if (error) { + console.log(error.message); + return; + } + console.log(data); + }); + }, []); + const searchParams = useSearchParams(); - const email = searchParams.get('email') || 'xxxx-xxxx-xxxx-xxxx'; + const user_email = searchParams.get('user_email'); + + if (!user_email) return null; - if(!validateEmail(email)) { + if(!validateEmail(user_email)) { return (
@@ -23,7 +36,7 @@ export default function Confirm() { Invalid email address:

- {email} + {user_email}

); @@ -49,44 +62,47 @@ export default function Confirm() { }, [loading, countdown]); const resent = async () => { + const origin = window.location.origin; setLoading(true) const { error } = await supabase.auth.resend({ type: 'signup', - email: email, + email: user_email, options: { - emailRedirectTo: '/auth/confirm_email?email=' + email, + emailRedirectTo: `${origin}/auth/confirm`, } }) if (error) { setError(error.message) + } else { + setError(null) } } return (
-
+

Email Sent!

- We've sent a confirmation email to {email}. Click the link in + We've sent a confirmation email to {user_email}. Click the link in the email to verify your account.

- Return to Home + Return to Login
diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx index d3f1d0f..feb03da 100644 --- a/src/app/auth/page.tsx +++ b/src/app/auth/page.tsx @@ -26,6 +26,11 @@ export default function Auth(){ const auth = searchParams.get('auth'); const organisation = searchParams.get('organisation'); const oauthhidden = organisation==='iiitv'?true:false; + const err = searchParams.get('error'); + + useEffect(() => { + if (err) setError(err); + }, [err]); const cookies = useCookies(); const email = cookies.get('email') || null; @@ -48,22 +53,24 @@ export default function Auth(){ } else { router.push('/form_create'); } - } + } + const signup = async (cur: EventTarget & HTMLFormElement) => { const email = cur.email.value; const username = cur.username.value; const password = cur.password.value; - cookies.set('username', username); setError(null); - const { error } = await SignUp({email, password}) + const { user_id, error } = await SignUp({username, email, password}) + console.log(error); if (error) { setError(error); return false; } else { - router.push('/auth/confirm_email?email=' + email); + router.push(`/auth/confirm_email?id=${user_id}&user_email=${email}`); } } + const Authsignin = async () => { setError(null); const { error, url } = await AuthSignIn(); @@ -88,13 +95,13 @@ export default function Auth(){ EmailSubmit={emailSubmit} email={email}> - { auth && -
- {auth === 'signup' ? -

Have an account? Login

: -

Don't have an account? Create here

} -
- } + { auth && (
+
+ {auth === 'signup' ? +

Have an account? Login

: +

Don't have an account? Create here

} +
+
)} {oauthhidden && } diff --git a/src/app/form_create/component/styles.module.css b/src/app/form_create/component/styles.module.css new file mode 100644 index 0000000..e13e8fd --- /dev/null +++ b/src/app/form_create/component/styles.module.css @@ -0,0 +1,27 @@ +.error { + position: absolute; + left:6px; + top: -17.5px; + height: fit-content !important; + color:red !important; + font-size:0.7rem !important; + word-wrap: no-wrap !important; + white-space: nowrap !important; + z-index: 10; +} +.inputicon { + position:absolute; + top: 50%; + color: rgba(var(--black-light), .45); + font-size: 1rem; + transform: translateY(-50%); + transition: 0.5s ease; +} +.eyeicon { + right: 15px; + padding: 3px; + cursor: pointer; +} +.eyeicon:hover { + color: rgba(var(--black-light), .65); +} \ No newline at end of file diff --git a/src/app/form_create/component/userform.tsx b/src/app/form_create/component/userform.tsx new file mode 100644 index 0000000..c8d6e9d --- /dev/null +++ b/src/app/form_create/component/userform.tsx @@ -0,0 +1,120 @@ +import React, { useState, useEffect, useCallback, use } from "react"; +import axios from "axios"; + +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import Loader from '@/components/ui/loader' + +import styles from './styles.module.css' +import { cn } from "@/lib/utils"; + + +interface PropsType { + email: string | null, + user_id: string, +} + +export function UserForm( {user_id, email}: PropsType) { + const [username, setUsername] = useState(null); + const [error, setError] = useState(null); + const [disabled, setDisabled] = useState(true); + const [loading, setLoading] = useState(false); + + const controller = new AbortController(); + const signal = controller.signal; + + const isExist = useCallback(async () => { + const res = await axios.post('/api/rest/v1/isUsername', {username: username}, {signal}); + if (res.data.state) { + setDisabled(true); + setError('Username already exists'); + } else { + setDisabled(false); + setError(''); + } + }, [username]); + + + useEffect(() => { + let timer: NodeJS.Timeout; + if (username) { + timer = setTimeout(() => { + isExist(); + }, 400) + } + return () => { + controller.abort(); + clearTimeout(timer); + } + }, [username]); + + useEffect(() => { + if (!username) { + console.log('username is empty'); + setError(''); + setDisabled(true); + } + }, [username, disabled, error]); + + return ( +
+
+
+

+ Welcome, {email?.split('@')[0]} +

+

Please enter an username

+
+
{ + e.preventDefault(); + setLoading(true); + axios.post(`/api/rest/v1/users?option=insert`, {id: user_id, username: username}) + .then((res) => { + if (res.data.error) { + setError(res.data.error); + setLoading(false); + } else { + setLoading(false); + window.location.href = '/form_create'; + } + }) + .catch((error) => { + setError(error.message); + setLoading(false); + }); + }}> +
+ + setUsername(e.target.value)} + id={'username'} + name={'username'} + type={'text'} + autoComplete={'username'} + required + placeholder={'Username'} + disabled={loading} + className={cn("rounded-[8px] border border-input bg-background px-4 py-6 text-foreground placeholder-muted-foreground focus:z-10 focus:border-primary focus:outline-none sm:text-sm")} + /> + {error} +
+
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/form_create/page.tsx b/src/app/form_create/page.tsx index be456f2..18a0fc2 100644 --- a/src/app/form_create/page.tsx +++ b/src/app/form_create/page.tsx @@ -1,25 +1,118 @@ 'use client'; -import React from 'react'; +import React, { useState, useEffect, useCallback, use } from 'react'; +import Link from 'next/link'; +import { useSearchParams } from 'next/navigation'; import { supabase } from '@/utils/supabase/client'; +import { User } from '@supabase/supabase-js'; +import axios from 'axios'; + +import { UserForm } from './component/userform'; + import { cn } from '@/lib/utils'; -import { Button } from '@/components/ui/button'; +export default function Page() { + const [user, setUser] = useState(null); + const [username , setUsername] = useState(null); + const [useremail , setUserEmail] = useState(null); + const [error_message, setErrorMessage] = useState(null); + + const [usernameInput, setUsernameInput] = useState(false); + + const [loading, setLoading] = useState(false); + + const searchParams = useSearchParams(); + + useEffect(() => { + console.log() + const getUser = async () => { + const { data: { user }, error } = await supabase.auth.getUser(); + if (error) { + console.log(error.message); + return; + } + setUser(user); + setUserEmail(user?.email || null); + } + getUser(); + }, []); + + useEffect(() => { + return () => { + + if ( user ) { + getUserData(); + } + }; + }, [user]); + + const getUsernameFromUser = async () => { + if (searchParams.get('username')) { + const res = await axios.post('/api/rest/v1/isUsername', {username: searchParams.get('username')}); + if (!res.data.state) { + axios.post('/api/rest/v1/users?option=insert', {id: user?.id, username: searchParams.get('username')}) + .then((res) => { + if (res.data.error) { + setErrorMessage(res.data.error); + return {error: res.data.error}; + } + setUsername(searchParams.get('username')); + }) + return {error: null}; + } + } + setUsernameInput(true); + return {error: null}; + } + + let getUserData = useCallback(async () => { + try { + setLoading(true); + if (!user) throw new Error('No user on the session'); + + const { data, error, status } = await supabase + .from('users') + .select('username') + .eq('id', user.id) + .single(); -export default async function Page() { - const { data : {user}} = await supabase.auth.getUser(); - const { data, error } = await supabase.from('users').select('username').eq('id', user?.id); - const userdata = data && data[0]; - const username = userdata && ( 'username' in userdata && userdata.username || user?.identities); + if (status === 406) { + const { error } = await getUsernameFromUser() + if (error) throw error;return; + } + if (error && status !== 406 ) throw error; + if (data) { + setUsername(data.username); + } + } + catch (error: any) { + console.log(error.message); + } + finally { + setLoading(false); + } + } + , [user]); - const Logout = async () => { - const { error } = await supabase.auth.signOut(); - if (error) console.error('Sign out error', error.message); - window.location.reload(); - }; return ( -
-

Welcome, {username}

- -
+ <> + {usernameInput ? ( + + ):( +
+

Welcome, {username}

+

{useremail}

+ {error_message &&

{error_message}

} + {loading &&

Loading...

} + + Logout + +
+ )} + ); } \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4ff1260..d05d061 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -27,6 +27,11 @@ export default function RootLayout({ + }>{children} diff --git a/src/components/ui/loader.tsx b/src/components/ui/loader.tsx new file mode 100644 index 0000000..9456492 --- /dev/null +++ b/src/components/ui/loader.tsx @@ -0,0 +1,20 @@ +import { useEffect } from 'react' + +export default function Loader() { + useEffect(() => { + async function getLoader() { + const { ring } = await import('ldrs') + ring.register() + } + getLoader() + }, []) + return ( + + ) +} \ No newline at end of file diff --git a/src/utils/supabase/middleware.ts b/src/utils/supabase/middleware.ts index 842f875..ee926c6 100644 --- a/src/utils/supabase/middleware.ts +++ b/src/utils/supabase/middleware.ts @@ -1,5 +1,5 @@ import { createServerClient, type CookieOptions } from '@supabase/ssr' -import { NextResponse, type NextRequest } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' export async function updateSession(request: NextRequest) { let response = NextResponse.next({ @@ -54,16 +54,43 @@ export async function updateSession(request: NextRequest) { } ) - const { data: {user}} = await supabase.auth.getUser() - if (user && !user?.confirmed_at && request.nextUrl.pathname === '/form_create') { - response = NextResponse.redirect(new URL('/auth/confirm_email', request.nextUrl.href)) - } else if (user?.confirmed_at && request.nextUrl.pathname === '/auth/confirm_email') { - response = NextResponse.redirect(new URL('/form_create', request.nextUrl.href)) - } else if (user && request.nextUrl.pathname === '/auth') { + + if (request.nextUrl.pathname === '/auth/callback' || request.nextUrl.pathname === '/auth/confirm') return response + + + let { data: {user}, error} = await supabase.auth.getUser() + + + if (user && (request.nextUrl.pathname === '/auth/confirm_email' || request.nextUrl.pathname === '/auth')) { response = NextResponse.redirect(new URL('/form_create', request.nextUrl.href)) - } else if (!user && request.nextUrl.pathname === '/form_create') { + } else if (!user && ( request.nextUrl.pathname === '/form_create' )) { response = NextResponse.redirect(new URL('/auth', request.nextUrl.href)) } + const searchParams = request.nextUrl.searchParams + + if (request.nextUrl.pathname === '/auth/confirm_email') { + if (searchParams.get('user_email')) { + if (!searchParams.get('id')) return NextResponse.redirect(new URL('/auth?error=No+ID+provided', request.nextUrl.href)) + let res = await fetch(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/auth/v1/admin/users/${searchParams.get('id')}`, { + method: 'GET', + headers: { + "apikey": process.env.SERVICE_KEY!, + "Authorization": `Bearer ${process.env.SERVICE_KEY!}`, + "Content-Type": "application/json", + } + }); + + const res_user = await res.json(); + if (res.status === 404 || !res_user?.email ) return NextResponse.redirect(new URL('/auth?error=user+not+found', request.nextUrl.href)) + if ( request.nextUrl.searchParams.get('user_email') !== res_user?.email ) { + request.nextUrl.searchParams.set('user_email', res_user?.email as string) + response = NextResponse.redirect(new URL(request.nextUrl.href)) + return response + } + if (res_user?.email_confirmed_at) return NextResponse.redirect(new URL('/auth?auth=login', request.nextUrl.href)) + } + } + return response } \ No newline at end of file