mirror of
				https://github.com/r-freeman/portfolio.git
				synced 2025-11-04 10:51:11 +00:00 
			
		
		
		
	Add comment notifications
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Build And Publish / BuildAndPublish (push) Successful in 3m12s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Build And Publish / BuildAndPublish (push) Successful in 3m12s
				
			This commit is contained in:
		
							parent
							
								
									400e199c15
								
							
						
					
					
						commit
						053a5d3768
					
				@ -9,3 +9,8 @@ GITHUB_SECRET=
 | 
				
			|||||||
NEXT_PUBLIC_SUPABASE_URL=
 | 
					NEXT_PUBLIC_SUPABASE_URL=
 | 
				
			||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=
 | 
					NEXT_PUBLIC_SUPABASE_ANON_KEY=
 | 
				
			||||||
SUPABASE_SERVICE_ROLE_KEY=
 | 
					SUPABASE_SERVICE_ROLE_KEY=
 | 
				
			||||||
 | 
					AUTH_SECRET=
 | 
				
			||||||
 | 
					AUTH_TRUST_HOST=
 | 
				
			||||||
 | 
					AUTH_REDIRECT_PROXY_URL=
 | 
				
			||||||
 | 
					NTFY_URL=
 | 
				
			||||||
 | 
					NTFY_TOKEN=
 | 
				
			||||||
@ -3,65 +3,80 @@
 | 
				
			|||||||
import {auth, signIn} from '@/auth'
 | 
					import {auth, signIn} from '@/auth'
 | 
				
			||||||
import {createClient} from '@/lib/supabase/server'
 | 
					import {createClient} from '@/lib/supabase/server'
 | 
				
			||||||
import {z} from 'zod'
 | 
					import {z} from 'zod'
 | 
				
			||||||
 | 
					import {sendNotification} from '@/lib/ntfy'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function loginWithGitHub() {
 | 
					export async function loginWithGitHub() {
 | 
				
			||||||
    await signIn('github')
 | 
					    await signIn('github')
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const notificationBody = (comment: any, user: any) => {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        topic: 'comments',
 | 
				
			||||||
 | 
					        message: `You have a new comment from ${user.name}:\n${comment.content}\n`,
 | 
				
			||||||
 | 
					        actions: [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                action: 'http',
 | 
				
			||||||
 | 
					                label: 'Approve comment',
 | 
				
			||||||
 | 
					                url: `${process.env.NEXT_PUBLIC_SITE_URL}/api/comments/moderate/${comment.id}`,
 | 
				
			||||||
 | 
					                method: 'PATCH',
 | 
				
			||||||
 | 
					                headers: {
 | 
				
			||||||
 | 
					                    Authorization: `Bearer ${process.env.NTFY_TOKEN}`
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function addComment(prevState: { message: string }, formData: FormData) {
 | 
					export async function addComment(prevState: { message: string }, formData: FormData) {
 | 
				
			||||||
 | 
					    const general_error = 'There was an error with your comment, please try again later.'
 | 
				
			||||||
 | 
					    const authorisation_error = 'Error, you must be logged in to post a comment.'
 | 
				
			||||||
 | 
					    const success_message = 'Your comment was submitted and is awaiting approval.'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const schema = z.object({
 | 
					    const schema = z.object({
 | 
				
			||||||
        comment: z.string().min(3).max(255),
 | 
					        comment: z.string().min(3).max(255),
 | 
				
			||||||
        slug: z.string()
 | 
					        slug: z.string()
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const parse = schema.safeParse({
 | 
					    const {comment, slug} = {
 | 
				
			||||||
        comment: formData.get('comment'),
 | 
					        comment: formData.get('comment'),
 | 
				
			||||||
        slug: formData.get('slug')
 | 
					        slug: formData.get('slug')
 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let message = ''
 | 
					 | 
				
			||||||
    if (!parse.success) {
 | 
					 | 
				
			||||||
        message = 'There was an error with your comment, please try again later.'
 | 
					 | 
				
			||||||
        return {message: message}
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const parse = schema.safeParse({comment, slug})
 | 
				
			||||||
 | 
					    if (!parse.success) {
 | 
				
			||||||
 | 
					        return {message: general_error}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
        const supabase = await createClient()
 | 
					        const supabase = await createClient()
 | 
				
			||||||
        const session = await auth()
 | 
					        const session = await auth()
 | 
				
			||||||
    const slug = formData.get('slug')
 | 
					 | 
				
			||||||
    const content = formData.get('comment')
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (session?.user) {
 | 
					        if (!session?.user) {
 | 
				
			||||||
 | 
					            return {message: authorisation_error}
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const {name, email, image} = session.user
 | 
					        const {name, email, image} = session.user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const [{data: user}, {data: article}] = await Promise.all([
 | 
					        const [{data: user}, {data: article}] = await Promise.all([
 | 
				
			||||||
            supabase.from('users')
 | 
					            supabase.from('users').upsert({name, email, image}, {onConflict: 'email'}).select('*').single(),
 | 
				
			||||||
                .upsert({name, email, image}, {onConflict: 'email'})
 | 
					            supabase.from('articles').select('id').eq('slug', slug).single()
 | 
				
			||||||
                .select('id')
 | 
					 | 
				
			||||||
                .single(),
 | 
					 | 
				
			||||||
            supabase.from('articles')
 | 
					 | 
				
			||||||
                .select('id')
 | 
					 | 
				
			||||||
                .eq('slug', slug)
 | 
					 | 
				
			||||||
                .single()
 | 
					 | 
				
			||||||
        ])
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (user?.id && article?.id) {
 | 
					        const {data: newComment, error} = await supabase
 | 
				
			||||||
            const {data: comment} = await supabase
 | 
					 | 
				
			||||||
            .from('comments')
 | 
					            .from('comments')
 | 
				
			||||||
                .insert({content: content, article_id: article.id, user_id: user.id})
 | 
					            .insert({content: comment, article_id: article?.id, user_id: user?.id})
 | 
				
			||||||
                .select('id')
 | 
					            .select('*')
 | 
				
			||||||
            .single()
 | 
					            .single()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (comment?.id === null) {
 | 
					        if (error || newComment?.id === null) {
 | 
				
			||||||
                message = 'There was an error with your comment, please try again later.'
 | 
					            return {message: general_error}
 | 
				
			||||||
                return {
 | 
					 | 
				
			||||||
                    message: message
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    message = 'Your comment was posted successfully.'
 | 
					        await sendNotification(notificationBody(newComment, user))
 | 
				
			||||||
    return {
 | 
					
 | 
				
			||||||
        message
 | 
					        return {message: success_message}
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.error('Error posting comment:', error)
 | 
				
			||||||
 | 
					        return {message: general_error}
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -17,7 +17,7 @@ export async function GET(request: Request, {params}: { params: Promise<{ slug:
 | 
				
			|||||||
                    article:articles!inner(id, slug)
 | 
					                    article:articles!inner(id, slug)
 | 
				
			||||||
                `)
 | 
					                `)
 | 
				
			||||||
                .eq('article.slug', slug)
 | 
					                .eq('article.slug', slug)
 | 
				
			||||||
                // .eq('published', 'true')
 | 
					                .eq('published', true)
 | 
				
			||||||
                .order('created_at', {ascending: false})
 | 
					                .order('created_at', {ascending: false})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (comments !== null && comments?.length > 0) {
 | 
					            if (comments !== null && comments?.length > 0) {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										23
									
								
								app/api/comments/moderate/[id]/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/api/comments/moderate/[id]/route.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					import {createClient} from '@/lib/supabase/server'
 | 
				
			||||||
 | 
					import {headers} from 'next/headers'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function PATCH(request: Request, {params}: { params: Promise<{ id: number }> }) {
 | 
				
			||||||
 | 
					    const {id} = await params
 | 
				
			||||||
 | 
					    const headersList = await headers()
 | 
				
			||||||
 | 
					    const authorizationHeader = headersList.get('authorization')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (authorizationHeader === `Bearer ${process.env.NTFY_TOKEN}`) {
 | 
				
			||||||
 | 
					        if (typeof id !== 'undefined') {
 | 
				
			||||||
 | 
					            const supabase = await createClient()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await supabase.from('comments')
 | 
				
			||||||
 | 
					                .update({published: true})
 | 
				
			||||||
 | 
					                .eq('id', id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return new Response(null, {status: 204})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return new Response(JSON.stringify({status: 'Not Found'}), {status: 404})
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(JSON.stringify({status: 'Unauthorized'}), {status: 401})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15
									
								
								lib/ntfy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								lib/ntfy.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					import fetcher from '@/lib/fetcher'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NTFY_URL = process.env.NTFY_URL || ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function sendNotification(notificationBody: any) {
 | 
				
			||||||
 | 
					    if (NTFY_URL !== '') {
 | 
				
			||||||
 | 
					        await fetcher(NTFY_URL, {
 | 
				
			||||||
 | 
					            method: 'POST',
 | 
				
			||||||
 | 
					            headers: {
 | 
				
			||||||
 | 
					                'Content-Type': 'application/json'
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            body: JSON.stringify(notificationBody)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user