diff --git a/.gitignore b/.gitignore
index c74ff1b..07e6e47 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1 @@
-.env
/node_modules
diff --git a/.notes..swp b/.notes..swp
new file mode 100644
index 0000000..e44a7cf
Binary files /dev/null and b/.notes..swp differ
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..c2a77cc
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "workbench.colorTheme": "G Dark (Haiti)"
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 2c3ace4..755e18f 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
-# NextJS Supabase Dashboard
+########## Appovideo Libay Manager Tool
+## via NextJS Supabase Dashboard
This is a dashboard starter template for the [NextJS](https://nextjs.org) 14 app router using supabase based on [shadcn-ui](https://ui.shadcn.com).
diff --git a/app/api/auth/callback/route.ts b/app/api/auth/callback/route.ts
index cf7b868..f606e05 100644
--- a/app/api/auth/callback/route.ts
+++ b/app/api/auth/callback/route.ts
@@ -1,45 +1,25 @@
+// @/app/api/auth/callback/route.ts
+import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
-import { type CookieOptions, createServerClient } from '@supabase/ssr'
-/**
- * OAuth with PKCE flow for SSR
- *
- * @link https://supabase.com/docs/guides/auth/server-side/oauth-with-pkce-flow-for-ssr
- */
export async function GET(request: Request) {
- const { searchParams, origin } = new URL(request.url)
- const code = searchParams.get('code')
- // if "next" is in param, use it as the redirect URL
- const next = searchParams.get('next') ?? '/'
+ const requestUrl = new URL(request.url)
+ const code = requestUrl.searchParams.get('code')
+ const next = requestUrl.searchParams.get('next') || '/dashboard'
if (code) {
- const cookieStore = cookies()
- const supabase = createServerClient(
- process.env.NEXT_PUBLIC_SUPABASE_URL!,
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
- {
- cookies: {
- get(name: string) {
- return cookieStore.get(name)?.value
- },
- set(name: string, value: string, options: CookieOptions) {
- cookieStore.set({ name, value, ...options })
- },
- remove(name: string, options: CookieOptions) {
- cookieStore.delete({ name, ...options })
- },
- },
- }
- )
-
+ const supabase = createRouteHandlerClient({ cookies })
const { error } = await supabase.auth.exchangeCodeForSession(code)
-
- if (!error) {
- return NextResponse.redirect(`${origin}${next}`)
+
+ if (error) {
+ console.error('Auth callback error:', error)
+ return NextResponse.redirect(
+ new URL('/auth/signin?error=' + encodeURIComponent(error.message), request.url)
+ )
}
}
- // return the user to an error page with instructions
- return NextResponse.redirect(`${origin}/auth/auth-code-error`)
-}
+ // Redirect to the dashboard or specified next page
+ return NextResponse.redirect(new URL(next, request.url))
+}
\ No newline at end of file
diff --git a/app/api/auth/confirm/route.ts b/app/api/auth/confirm/route.ts
index bd2c07d..a2fbdf6 100644
--- a/app/api/auth/confirm/route.ts
+++ b/app/api/auth/confirm/route.ts
@@ -1,23 +1,23 @@
+// app/api/auth/confirm/route.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { type EmailOtpType } from '@supabase/supabase-js'
import { cookies } from 'next/headers'
import { NextResponse, type NextRequest } from 'next/server'
-/**
- * Email Auth with PKCE flow for SSR
- *
- * @link https://supabase.com/docs/guides/auth/server-side/email-based-auth-with-pkce-flow-for-ssr
- */
-export async function GET(request: NextRequest) {
- const { searchParams } = new URL(request.url)
- const token_hash = searchParams.get('token_hash') as string
+export async function GET(request: NextRequest)
+ {
+ try {
+ const { searchParams, origin } = new URL(request.url) // Define origin here
+ const token_hash = searchParams.get('token_hash')
const type = searchParams.get('type') as EmailOtpType | null
// if "next" is in param, use it as the redirect URL
const next = (searchParams.get('next') as string) ?? '/'
const redirectTo = request.nextUrl.clone()
redirectTo.pathname = next
- if (token_hash && type) {
+ if (!token_hash || !type) {
+ throw new Error('Missing token_hash or type')
+ }
const cookieStore = cookies()
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
@@ -39,12 +39,14 @@ export async function GET(request: NextRequest) {
const { error } = await supabase.auth.verifyOtp({ type, token_hash })
- if (!error) {
- return NextResponse.redirect(redirectTo)
+ if (error) {
+ throw error // Re-throw the error to be caught by the catch block
}
- }
- // return the user to an error page with some instructions
- redirectTo.pathname = '/auth/auth-code-error'
- return NextResponse.redirect(redirectTo)
+ return NextResponse.redirect(redirectTo)
+ } catch (error) {
+ console.error('Error in /auth/confirm:', error)
+ const redirectTo = new URL('/auth/auth-code-error', request.nextUrl.origin)
+ return NextResponse.redirect(redirectTo)
+ }
}
diff --git a/app/auth-test/page.tsx b/app/auth-test/page.tsx
new file mode 100644
index 0000000..574988a
--- /dev/null
+++ b/app/auth-test/page.tsx
@@ -0,0 +1,12 @@
+
+// @/app/auth-test/page.tsx
+import AuthTest from '@/components/auth-test'
+
+export default function TestPage() {
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx
new file mode 100644
index 0000000..e7ae203
--- /dev/null
+++ b/app/auth/callback/page.tsx
@@ -0,0 +1,48 @@
+// app/auth/callback/page.tsx
+import { createServerClient } from '@supabase/ssr'
+import { cookies } from 'next/headers'
+import { redirect } from 'next/navigation'
+
+export const dynamic = 'force-dynamic'
+
+export default async function AuthCallbackPage({
+ searchParams,
+}: {
+ searchParams: { code: string; next?: string }
+}) {
+ const cookieStore = cookies()
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ get(name: string) {
+ return cookieStore.get(name)?.value
+ },
+ set(name: string, value: string, options: any) {
+ cookieStore.set({ name, value, ...options })
+ },
+ remove(name: string, options: any) {
+ cookieStore.delete({ name, ...options })
+ },
+ },
+ }
+ )
+
+ const code = searchParams.code
+
+ try {
+ if (code) {
+ const { error } = await supabase.auth.exchangeCodeForSession(code)
+ if (error) {
+ throw error
+ }
+ }
+
+ // URL to redirect to after sign in process completes
+ return redirect(searchParams.next || '/dashboard')
+ } catch (error) {
+ console.error('Auth callback error:', error)
+ return redirect('/auth/auth-code-error')
+ }
+}
\ No newline at end of file
diff --git a/app/auth/signin/signin-form.tsx b/app/auth/signin/signin-form.tsx
index dc351e6..c75e6fd 100644
--- a/app/auth/signin/signin-form.tsx
+++ b/app/auth/signin/signin-form.tsx
@@ -44,12 +44,44 @@ const SignInForm = () => {
defaultValues,
})
+
+ // Add dev login function
+ const handleDevLogin = async () => {
+ const supabase = createClient()
+ try {
+ const { data, error } = await supabase.auth.signInWithPassword({
+ email: 'dev@approvideo.org', // Replace with your dev email
+ password: 'devpass123' // Replace with your dev password
+ })
+
+ if (error) throw error
+
+ toast.success('Dev login successful')
+ window.location.href = '/dashboard' // Force reload to ensure auth state
+ } catch (error) {
+ console.error('Dev login failed:', error)
+ toast.error('Dev login failed')
+ }
+ }
+
+
return (
)
diff --git a/app/auth/signup/signup-form.tsx b/app/auth/signup/signup-form.tsx
index 181db8b..2b3f8f4 100644
--- a/app/auth/signup/signup-form.tsx
+++ b/app/auth/signup/signup-form.tsx
@@ -1,13 +1,12 @@
+// app/auth/signup/signup-form.tsx
'use client'
import * as React from 'react'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
-
import { useForm, useFormContext } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
-
import { toast } from 'sonner'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
@@ -20,16 +19,30 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form'
-
import { createClient } from '@/supabase/client'
import { useAuth } from '@/hooks/use-auth'
+// Enhanced password validation
+const passwordSchema = z
+ .string()
+ .nonempty('Password is required')
+ .min(8, 'Password must be at least 8 characters')
+ .max(72, 'Password must not exceed 72 characters')
+ .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
+ .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
+ .regex(/[0-9]/, 'Password must contain at least one number')
+ .regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character')
+
const FormSchema = z
.object({
- email: z.string().nonempty().max(255).email(),
- // If the password is larger than 72 chars, it will be truncated to the first 72 chars.
- newPassword: z.string().nonempty().min(6).max(72),
- confirmNewPassword: z.string().nonempty().min(6).max(72),
+ email: z
+ .string()
+ .nonempty('Email is required')
+ .email('Invalid email format')
+ .max(255, 'Email must not exceed 255 characters')
+ .transform(val => val.toLowerCase().trim()),
+ newPassword: passwordSchema,
+ confirmNewPassword: passwordSchema,
})
.refine((val) => val.newPassword === val.confirmNewPassword, {
path: ['confirmNewPassword'],
@@ -44,18 +57,63 @@ const defaultValues: Partial = {
confirmNewPassword: '',
}
+
+
+
+
+
+// Password strength indicator
+const PasswordStrengthIndicator = ({ password }: { password: string }) => {
+ const strength = React.useMemo(() => {
+ if (!password) return 0
+ let score = 0
+ if (password.length >= 8) score++
+ if (/[A-Z]/.test(password)) score++
+ if (/[a-z]/.test(password)) score++
+ if (/[0-9]/.test(password)) score++
+ if (/[^A-Za-z0-9]/.test(password)) score++
+ return score
+ }, [password])
+
+ return (
+
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+
+ {strength === 0 && 'Very weak'}
+ {strength === 1 && 'Weak'}
+ {strength === 2 && 'Fair'}
+ {strength === 3 && 'Good'}
+ {strength === 4 && 'Strong'}
+ {strength === 5 && 'Very strong'}
+
+
+ )
+}
+
const SignUpForm = () => {
const form = useForm({
resolver: zodResolver(FormSchema),
- mode: 'onSubmit',
+ mode: 'onChange', // Enable real-time validation
defaultValues,
})
+ const password = form.watch('newPassword')
+
return (
@@ -71,7 +129,7 @@ const EmailField = () => {
(
+ render={({ field, fieldState }) => (
{t('email')}
@@ -81,9 +139,13 @@ const EmailField = () => {
autoComplete="email"
autoCorrect="off"
placeholder="name@example.com"
+ className={fieldState.error ? 'border-red-500' : ''}
{...field}
/>
+
+ You'll need to verify this email
+
)}
@@ -94,24 +156,38 @@ const EmailField = () => {
const NewPasswordField = () => {
const { t } = useTranslation()
const { control } = useFormContext()
+ const [showPassword, setShowPassword] = React.useState(false)
return (
(
+ render={({ field, fieldState }) => (
{t('password')}
-
-
-
+
+
+
+
+
+
+
+ Must include uppercase, lowercase, number and special character
+
)}
@@ -122,24 +198,35 @@ const NewPasswordField = () => {
const ConfirmNewPasswordField = () => {
const { t } = useTranslation()
const { control } = useFormContext()
+ const [showPassword, setShowPassword] = React.useState(false)
return (
(
+ render={({ field, fieldState }) => (
{t('confirm_password')}
-
-
-
+
+
+
+
+
+
)}
@@ -150,42 +237,77 @@ const ConfirmNewPasswordField = () => {
const SubmitButton = () => {
const router = useRouter()
const { t } = useTranslation()
- const { handleSubmit, setError, getValues } = useFormContext()
const { setSession, setUser } = useAuth()
-
const [isSubmitting, setIsSubmitting] = React.useState(false)
+ const { handleSubmit, setError, getValues, formState } = useFormContext() // Added formState here
const onSubmit = async () => {
try {
setIsSubmitting(true)
-
const formValues = getValues()
+ // Log signup attempt (excluding password)
+ console.log('Attempting signup:', {
+ email: formValues?.email,
+ timestamp: new Date().toISOString()
+ })
+
const supabase = createClient()
- const signed = await supabase.auth.signUp({
+ const { data, error } = await supabase.auth.signUp({
email: formValues?.email,
password: formValues?.newPassword,
+ options: {
+ emailRedirectTo: `${window.location.origin}/auth/callback`,
+ data: {
+ email_confirmed: false,
+ signup_date: new Date().toISOString()
+ }
+ }
})
- if (signed?.error) throw new Error(signed?.error?.message)
- const unsigned = await supabase.auth.signOut()
- if (unsigned?.error) throw new Error(unsigned?.error?.message)
+ if (error) {
+ console.error('Signup error:', {
+ code: error.status,
+ message: error.message,
+ timestamp: new Date().toISOString()
+ })
+ throw error
+ }
+
+ if (data?.user) {
+ console.log('Signup successful:', {
+ userId: data.user.id,
+ timestamp: new Date().toISOString()
+ })
- setSession(null)
- setUser(null)
+ // Sign out after successful registration
+ const unsigned = await supabase.auth.signOut()
+ if (unsigned?.error) throw new Error(unsigned?.error?.message)
- toast.success(t('you_have_successfully_registered_as_a_member'))
+ setSession(null)
+ setUser(null)
- router.refresh()
- router.replace('/auth/signin')
+ toast.success(t('you_have_successfully_registered_as_a_member'))
+ router.refresh()
+ router.replace('/auth/signin')
+ }
} catch (e: unknown) {
const err = (e as Error)?.message
- if (err.startsWith('User already registered')) {
+ console.error('Signup error details:', {
+ error: err,
+ timestamp: new Date().toISOString()
+ })
+
+ if (err.includes('already registered')) {
setError('email', {
message: t('user_already_registered'),
})
+ } else if (err.includes('password')) {
+ setError('newPassword', {
+ message: err
+ })
} else {
- toast.error(err)
+ toast.error(typeof err === 'string' ? err : 'Signup failed')
}
} finally {
setIsSubmitting(false)
@@ -196,12 +318,19 @@ const SubmitButton = () => {
)
}
-export { SignUpForm }
+export { SignUpForm }
\ No newline at end of file
diff --git a/app/dashboard/admin/videos/page.tsx b/app/dashboard/admin/videos/page.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/app/dashboard/admin/videos/ubmit-form.tsx b/app/dashboard/admin/videos/ubmit-form.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/app/dashboard/admin/videos/video-table.tx b/app/dashboard/admin/videos/video-table.tx
new file mode 100644
index 0000000..bc6448c
--- /dev/null
+++ b/app/dashboard/admin/videos/video-table.tx
@@ -0,0 +1,130 @@
+// app/dashboard/admin/videos/video-table.tsx
+'use client'
+
+import { useState } from 'react'
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table'
+import { Button } from '@/components/ui/button'
+import { createClient } from '@/supabase/client'
+
+interface Video {
+ id: string
+ title: string
+ url: string
+ description: string
+ status: 'pending' | 'approved' | 'rejected'
+ created_at: string
+ updated_at: string
+}
+
+export default function VideoTable() {
+ const [videos, setVideos] = useState