87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]',
92 | className,
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = 'TableCell'
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = 'TableCaption'
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | contact@byteshare.io.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Attribution
70 |
71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
72 | version 2.0, available at
73 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
74 |
75 | [homepage]: https://www.contributor-covenant.org
76 |
77 | For answers to common questions about this code of conduct, see the FAQ at
78 | https://www.contributor-covenant.org/faq. Translations are available at
79 | https://www.contributor-covenant.org/translations.
80 |
--------------------------------------------------------------------------------
/ui/src/app/(pages)/auth/forgot-password/components/forgot-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import appwriteService from '@/authentication/appwrite/config'
4 | import { Icons } from '@/components/icons'
5 | import { Button } from '@/components/ui/button'
6 | import { Input } from '@/components/ui/input'
7 | import { Label } from '@/components/ui/label'
8 | import { cn } from '@/lib/utils'
9 | import { useRouter } from 'next/navigation'
10 | import {
11 | FormEvent,
12 | HTMLAttributes,
13 | useEffect,
14 | useRef,
15 | useState,
16 | } from 'react'
17 | import { toast } from 'sonner'
18 |
19 | interface ForgotPasswordFormProps extends HTMLAttributes {}
20 |
21 | export function ForgotPasswordForm({
22 | className,
23 | ...props
24 | }: ForgotPasswordFormProps) {
25 | const router = useRouter()
26 | const [formData, setFormData] = useState({
27 | email: '',
28 | })
29 | const [isLoading, setIsLoading] = useState(false)
30 | let forgotPasswordEmail = ''
31 | if (typeof window !== 'undefined') {
32 | forgotPasswordEmail = sessionStorage.getItem('FORGOT_PASSWORD_EMAIL')
33 | }
34 | const audioRef = useRef(null)
35 |
36 | const playSound = () => {
37 | if (audioRef.current) {
38 | audioRef.current.play()
39 | }
40 | }
41 |
42 | useEffect(() => {
43 | if (forgotPasswordEmail) {
44 | setFormData({
45 | email: forgotPasswordEmail,
46 | })
47 | }
48 | }, [])
49 |
50 | const forgotPassword = async (e: FormEvent) => {
51 | e.preventDefault()
52 | setIsLoading(true)
53 | if (typeof window !== 'undefined') {
54 | sessionStorage.setItem('FORGOT_PASSWORD_EMAIL', '')
55 | }
56 |
57 | try {
58 | const response = await appwriteService.initiateForgotPassword(formData)
59 | if (response) {
60 | playSound()
61 | setFormData({ ...formData, email: '' })
62 | toast.info('Verification email has been sent.')
63 | }
64 | } catch (err: any) {
65 | toast.error(err.message)
66 | }
67 |
68 | setIsLoading(false)
69 | }
70 |
71 | return (
72 |
73 |
102 |
router.back()}
106 | >
107 | Go back
108 |
109 |
110 |
111 | )
112 | }
113 |
114 | export default ForgotPasswordForm
115 |
--------------------------------------------------------------------------------
/ui/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { Drawer as DrawerPrimitive } from 'vaul'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Drawer = ({
9 | shouldScaleBackground = true,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
16 | )
17 | Drawer.displayName = 'Drawer'
18 |
19 | const DrawerTrigger = DrawerPrimitive.Trigger
20 |
21 | const DrawerPortal = DrawerPrimitive.Portal
22 |
23 | const DrawerClose = DrawerPrimitive.Close
24 |
25 | const DrawerOverlay = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
34 | ))
35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
36 |
37 | const DrawerContent = React.forwardRef<
38 | React.ElementRef,
39 | React.ComponentPropsWithoutRef
40 | >(({ className, children, ...props }, ref) => (
41 |
42 |
43 |
51 |
52 | {children}
53 |
54 |
55 | ))
56 | DrawerContent.displayName = 'DrawerContent'
57 |
58 | const DrawerHeader = ({
59 | className,
60 | ...props
61 | }: React.HTMLAttributes) => (
62 |
66 | )
67 | DrawerHeader.displayName = 'DrawerHeader'
68 |
69 | const DrawerFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
77 | )
78 | DrawerFooter.displayName = 'DrawerFooter'
79 |
80 | const DrawerTitle = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
92 | ))
93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName
94 |
95 | const DrawerDescription = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, ...props }, ref) => (
99 |
104 | ))
105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName
106 |
107 | export {
108 | Drawer,
109 | DrawerPortal,
110 | DrawerOverlay,
111 | DrawerTrigger,
112 | DrawerClose,
113 | DrawerContent,
114 | DrawerHeader,
115 | DrawerFooter,
116 | DrawerTitle,
117 | DrawerDescription,
118 | }
119 |
--------------------------------------------------------------------------------
/worker/content-moderation/app/main.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import os
3 |
4 | import boto3
5 | import pika
6 | import requests
7 | import scanner.scan as scan
8 | import utils.logger as logger
9 | from dotenv import load_dotenv
10 |
11 | load_dotenv()
12 |
13 |
14 | def get_file_stream_from_r2(file_name):
15 | s3_object = r2.get_object(Bucket=bucket_name, Key=f"{file_name}")
16 | streaming_data = s3_object["Body"]
17 |
18 | return streaming_data
19 |
20 |
21 | def scan_upload(ch, method, properties, body):
22 | try:
23 | upload_id = str(body.decode("utf-8"))
24 | middleware_url = (
25 | os.getenv("MIDDLEWARE_BASE_URL") + "/" + "secured/scan/finalise/" + upload_id
26 | )
27 |
28 | files_in_folder = [
29 | obj["Key"]
30 | for obj in r2.list_objects(Bucket=bucket_name, Prefix=upload_id).get(
31 | "Contents", []
32 | )
33 | ]
34 |
35 | credentials = f"{os.getenv("SCAN_USER")}:{os.getenv("SCAN_PASS")}"
36 | encoded_credentials = base64.b64encode(credentials.encode("utf-8")).decode("ascii")
37 | authorization_header_value = f"Basic {encoded_credentials}"
38 |
39 | for file_name in files_in_folder:
40 | log.info(f"Scanning file: {file_name}")
41 | file_stream = get_file_stream_from_r2(file_name)
42 | passed = scan.scan_file(file_name, file_stream)
43 |
44 | if not passed:
45 | payload = {
46 | "safe": False,
47 | }
48 | headers = {
49 | "Content-Type": "application/json",
50 | "x-api-key": os.getenv("MIDDLEWARE_API_KEY"),
51 | "Authorization": authorization_header_value
52 | }
53 | response = requests.post(middleware_url, headers=headers, json=payload)
54 |
55 | log.info("NOT passed for Upload ID: " + upload_id)
56 | return
57 |
58 | payload = {
59 | "safe": True,
60 | }
61 |
62 | headers = {
63 | "Content-Type": "application/json",
64 | "x-api-key": os.getenv("MIDDLEWARE_API_KEY"),
65 | "Authorization": authorization_header_value
66 | }
67 | response = requests.post(middleware_url, headers=headers, json=payload)
68 | log.info("Passed for Upload ID: " + upload_id)
69 |
70 | except Exception as e:
71 | log.error(str(e))
72 | finally:
73 | ch.basic_ack(delivery_tag=method.delivery_tag)
74 |
75 |
76 | if __name__ == "__main__":
77 | log = logger.get_logger()
78 |
79 | endpoint_url = f"https://{os.getenv('R2_ACCOUNT_ID')}.r2.cloudflarestorage.com"
80 | bucket_name = os.getenv("R2_BUCKET_NAME")
81 | r2 = boto3.client(
82 | "s3",
83 | endpoint_url=endpoint_url,
84 | aws_access_key_id=os.getenv("R2_ACCESS_KEY"),
85 | aws_secret_access_key=os.getenv("R2_SECRET_KEY"),
86 | region_name="auto",
87 | )
88 |
89 | params = pika.URLParameters(os.getenv("RABBITMQ_URL"))
90 |
91 | connection = pika.BlockingConnection(params)
92 | channel = connection.channel()
93 | channel.queue_declare(queue=os.getenv("RABBITMQ_QUEUE"))
94 | channel.basic_consume(
95 | queue=os.getenv("RABBITMQ_QUEUE"),
96 | on_message_callback=scan_upload,
97 | auto_ack=False,
98 | )
99 | channel.basic_qos(prefetch_count=2)
100 |
101 | log.info("Content Management Started...")
102 | channel.start_consuming()
103 |
--------------------------------------------------------------------------------
/ui/src/app/(pages)/auth/reset-password/components/reset-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import appwriteService from '@/authentication/appwrite/config'
4 | import { Icons } from '@/components/icons'
5 | import { Button } from '@/components/ui/button'
6 | import { Input } from '@/components/ui/input'
7 | import { Label } from '@/components/ui/label'
8 | import { cn } from '@/lib/utils'
9 | import { useRouter, useSearchParams } from 'next/navigation'
10 | import { FormEvent, HTMLAttributes, useEffect, useState } from 'react'
11 | import { toast } from 'sonner'
12 |
13 | interface ResetPasswordFormProps extends HTMLAttributes {}
14 |
15 | export function ResetPasswordForm({
16 | className,
17 | ...props
18 | }: ResetPasswordFormProps) {
19 | const router = useRouter()
20 | const searchParams = useSearchParams()
21 | const [formData, setFormData] = useState({
22 | userId: '',
23 | secret: '',
24 | password: '',
25 | confirmPassword: '',
26 | })
27 |
28 | const [isLoading, setIsLoading] = useState(false)
29 |
30 | let userId = searchParams.get('userId')
31 | let secret = searchParams.get('secret')
32 | if (!userId || !secret) {
33 | router.push('/')
34 | }
35 |
36 | useEffect(() => {
37 | setFormData((prev) => ({
38 | ...prev,
39 | userId: userId,
40 | secret: secret,
41 | }))
42 | }, [])
43 |
44 | const resetPassword = async (e: FormEvent) => {
45 | e.preventDefault()
46 |
47 | userId = searchParams.get('userId')
48 | secret = searchParams.get('secret')
49 |
50 | setIsLoading(true)
51 |
52 | try {
53 | const response = await appwriteService.completeForgotPassword(formData)
54 | if (response) {
55 | router.push('/auth/login')
56 | }
57 | } catch (err: any) {
58 | toast.error(err.message)
59 | }
60 |
61 | setIsLoading(false)
62 | }
63 |
64 | return (
65 |
112 | )
113 | }
114 |
115 | export default ResetPasswordForm
116 |
--------------------------------------------------------------------------------
/ui/src/app/(pages)/auth/signup/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from 'next'
2 | import Image from 'next/image'
3 | import Link from 'next/link'
4 |
5 | import { buttonVariants } from '@/components/ui/button'
6 | import { cn } from '@/lib/utils'
7 | import { UserAuthForm } from './components/auth-form'
8 |
9 | export const metadata: Metadata = {
10 | title: 'Signup',
11 | description: 'Signup page',
12 | }
13 |
14 | function SignupPage() {
15 | return (
16 | <>
17 |
18 |
19 |
26 | Login
27 |
28 |
29 |
30 |
34 | {/*
44 |
45 | */}
46 |
52 | ByteShare
53 |
54 |
55 | {/*
56 |
57 | “This library has saved me countless hours of work and
58 | helped me deliver stunning designs to my clients faster than
59 | ever before.”
60 |
61 |
62 | */}
63 |
64 |
65 |
66 |
67 |
68 |
69 | Create your account
70 |
71 |
72 | Create your account by entering email and password.
73 |
74 |
75 |
76 |
77 | By continuing, you agree to our{' '}
78 |
82 | Terms of Service
83 | {' '}
84 | and{' '}
85 |
89 | Privacy Policy
90 |
91 | .
92 |
93 |
94 |
95 |
96 | >
97 | )
98 | }
99 |
100 | export default SignupPage
101 |
--------------------------------------------------------------------------------
/ui/public/json/terms-page-data.json:
--------------------------------------------------------------------------------
1 | {
2 | "termsOfUse": {
3 | "welcomeMessage": "Welcome to ByteShare!",
4 | "paragraphMessage":"These Terms of Use ('Terms') govern your access and use of our large file sharing platform ('Service'). By accessing or using the Service, you agree to be bound by these Terms. ",
5 | "note":"Please read them carefully before using the Service.",
6 | "sections": [
7 | {
8 | "title": "Acceptable Use",
9 | "content": [
10 | "You agree to use the Service only for lawful purposes and in accordance with these Terms. You agree not to:",
11 | "- Upload, store, share, or transmit any content that is illegal, harmful, threatening, abusive, harassing, discriminatory, or offensive.",
12 | "- Infringe on intellectual property rights, privacy rights, or other rights of others.",
13 | "- Use the Service in a way that could damage, disable, overburden, or impair the Service or any other related servers, networks, or systems.",
14 | "- Attempt to gain unauthorized access to the Service or any related systems or networks.",
15 | "- Impersonate any person or entity, or falsely state or misrepresent your affiliation with a person or entity.",
16 | "- Use the Service for any commercial purpose without our express written consent."
17 | ]
18 | },
19 | {
20 | "title": "File Storage and Deletion",
21 | "content": [
22 | "Files uploaded to the Service are stored for 60 days from the date of upload. After 60 days, your files will be automatically deleted.",
23 | "We reserve the right to delete any files that we deem to violate these Terms or applicable laws.",
24 | "We are not responsible for any loss or damage caused by the deletion of your files."
25 | ]
26 | },
27 | {
28 | "title": "Disclaimer and Warranties",
29 | "content": [
30 | "THE SERVICE IS PROVIDED 'AS IS' AND WITHOUT ANY WARRANTIES, EXPRESS OR IMPLIED. WE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.",
31 | "WE DO NOT WARRANT THAT THE SERVICE WILL BE UNINTERRUPTED, ERROR-FREE, OR VIRUS-FREE. WE DO NOT WARRANT THAT THE RESULTS THAT MAY BE OBTAINED FROM THE USE OF THE SERVICE WILL BE ACCURATE OR RELIABLE."
32 | ]
33 | },
34 | {
35 | "title": "Limitation of Liability",
36 | "content": [
37 | "WE WILL NOT BE LIABLE FOR ANY DAMAGES OF ANY KIND ARISING OUT OF OR RELATED TO YOUR USE OF THE SERVICE, INCLUDING, BUT NOT LIMITED TO, DIRECT, INDIRECT, INCIDENTAL, CONSEQUENTIAL, AND PUNITIVE DAMAGES."
38 | ]
39 | },
40 | {
41 | "title": "Termination",
42 | "content": [
43 | "We may terminate your access to the Service at any time, for any reason, without notice.",
44 | "You may terminate your use of the Service at any time."
45 | ]
46 | },
47 | {
48 | "title": "Governing Law and Dispute Resolution",
49 | "content": [
50 | "These Terms shall be governed by and construed in accordance with the laws of India, without regard to its conflict of laws provisions.",
51 | "Any dispute arising out of or relating to these Terms shall be subject to the exclusive jurisdiction of the courts of India."
52 | ]
53 | },
54 | {
55 | "title": "Entire Agreement",
56 | "content": [
57 | "These Terms constitute the entire agreement between you and us with respect to the Service and supersede all prior or contemporaneous communications, representations, or agreements, whether oral or written."
58 | ]
59 | },
60 | {
61 | "title": "Changes to Terms",
62 | "content": [
63 | "We may change these Terms at any time by posting the revised Terms on the Service. Your continued use of the Service after the posting of the revised Terms constitutes your acceptance of the revised Terms."
64 | ]
65 | }
66 | ]
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/ui/src/authentication/appwrite/config.ts:
--------------------------------------------------------------------------------
1 | import conf from '@/conf/config'
2 | import { Account, Client, ID } from 'appwrite'
3 |
4 | type CreateUserAccount = {
5 | name: string
6 | email: string
7 | password: string
8 | }
9 |
10 | type LoginUserAccount = {
11 | email: string
12 | password: string
13 | }
14 |
15 | type ForgotPassword = {
16 | email: string
17 | }
18 |
19 | type CompleteForgotPassword = {
20 | userId: string
21 | secret: string
22 | password: string
23 | confirmPassword: string
24 | }
25 |
26 | type CompleteVerification = {
27 | userId: string
28 | secret: string
29 | }
30 |
31 | type SignInWithGoogle = {
32 | successRedirect: string
33 | failureRedirect: string
34 | }
35 |
36 | const appwriteClient = new Client()
37 | appwriteClient.setEndpoint(conf.appwriteUrl).setProject(conf.appwriteProjectID)
38 |
39 | export const account = new Account(appwriteClient)
40 |
41 | export class AppwriteService {
42 | async createUserAccount({ name, email, password }: CreateUserAccount) {
43 | try {
44 | const userAccount = await account.create(
45 | ID.unique(),
46 | email,
47 | password,
48 | name,
49 | )
50 | if (userAccount) {
51 | return this.login({ email, password })
52 | } else {
53 | return userAccount
54 | }
55 | } catch (err: any) {
56 | throw err
57 | }
58 | }
59 |
60 | async login({ email, password }: LoginUserAccount) {
61 | try {
62 | return await account.createEmailSession(email, password)
63 | } catch (err: any) {
64 | throw err
65 | }
66 | }
67 |
68 | async loginWithGoogle({
69 | successRedirect,
70 | failureRedirect,
71 | }: SignInWithGoogle) {
72 | try {
73 | account.createOAuth2Session('google', successRedirect, failureRedirect)
74 | } catch (err: any) {
75 | throw err
76 | }
77 | }
78 |
79 | async initiateVerification() {
80 | try {
81 | const verifyEmailURL: string =
82 | process.env.NEXT_PUBLIC_APP_URL + '/auth/verify-email'
83 | return await account.createVerification(verifyEmailURL)
84 | } catch (err: any) {
85 | throw err
86 | }
87 | }
88 |
89 | async completeVerification({ userId, secret }: CompleteVerification) {
90 | try {
91 | return await account.updateVerification(userId, secret)
92 | } catch (err: any) {
93 | throw err
94 | }
95 | }
96 |
97 | async isLoggedIn(): Promise {
98 | try {
99 | const data = await this.getCurrentUser()
100 | return Boolean(data)
101 | } catch (err: any) {}
102 |
103 | return false
104 | }
105 |
106 | async getCurrentUser() {
107 | try {
108 | return account.get()
109 | } catch (err: any) {
110 | console.log('GetCurrentUser Error: ' + err)
111 | }
112 |
113 | return null
114 | }
115 |
116 | async getJWTToken(){
117 | try{
118 | const jwt = await account.createJWT()
119 | return jwt
120 | } catch (err: any) {
121 | console.log('GetJWTToken Error: ' + err)
122 | return null
123 | }
124 | }
125 |
126 | async initiateForgotPassword({ email }: ForgotPassword) {
127 | try {
128 | const resetPasswordURL: string =
129 | process.env.NEXT_PUBLIC_APP_URL + '/auth/reset-password'
130 | return await account.createRecovery(email, resetPasswordURL)
131 | } catch (err: any) {
132 | throw err
133 | }
134 | }
135 |
136 | // TODO: make it to self login
137 | async completeForgotPassword({
138 | userId,
139 | secret,
140 | password,
141 | confirmPassword,
142 | }: CompleteForgotPassword) {
143 | try {
144 | return await account.updateRecovery(
145 | userId,
146 | secret,
147 | password,
148 | confirmPassword,
149 | )
150 | } catch (err: any) {
151 | throw err
152 | }
153 | }
154 |
155 | async logout() {
156 | try {
157 | return await account.deleteSession('current')
158 | } catch (err: any) {
159 | console.log('Logout Error: ' + err)
160 | }
161 | }
162 | }
163 |
164 | const appwriteService = new AppwriteService()
165 |
166 | export default appwriteService
167 |
--------------------------------------------------------------------------------
/ui/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as DialogPrimitive from '@radix-ui/react-dialog'
5 | import { Cross2Icon } from '@radix-ui/react-icons'
6 |
7 | import { cn } from '@/lib/utils'
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = 'DialogHeader'
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = 'DialogFooter'
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/middleware/app/api/services/secured/developer.py:
--------------------------------------------------------------------------------
1 | import os
2 | import uuid
3 | from datetime import datetime, timezone
4 |
5 | import boto3
6 | import utils.logger as logger
7 | from database.db import DynamoDBManager
8 | from fastapi import HTTPException
9 |
10 | # Logger instance
11 | log = logger.get_logger()
12 |
13 | # DynamoDB
14 | table_name = "byteshare-apikey"
15 | apikey_dynamodb = DynamoDBManager(table_name)
16 |
17 | if os.getenv("ENVIRONMENT") == "production":
18 | api_gateway = boto3.client(
19 | "apigateway",
20 | region_name=os.getenv("AWS_APP_REGION"),
21 | )
22 | else:
23 | api_gateway = boto3.client(
24 | "apigateway",
25 | aws_access_key_id=os.getenv("AWS_APP_ACCESS_KEY"),
26 | aws_secret_access_key=os.getenv("AWS_APP_SECRET_ACCESS_KEY"),
27 | region_name=os.getenv("AWS_APP_REGION"),
28 | )
29 |
30 |
31 | def get_apikey_return_apikey(token_data):
32 | FUNCTION_NAME = "get_apikey_return_apikey()"
33 | log.info("Entering {}".format(FUNCTION_NAME))
34 |
35 | user_id = token_data["$id"]
36 |
37 | apikey_metadata = apikey_dynamodb.read_item({"user_id": user_id})
38 | exist = False
39 | if not apikey_metadata:
40 | exist = False
41 | else:
42 | exist = True
43 |
44 | log.info("Exiting {}".format(FUNCTION_NAME))
45 | return {"exist": exist}
46 |
47 |
48 | def create_apikey_return_apikey(token_data):
49 | FUNCTION_NAME = "create_apikey_return_apikey()"
50 | log.info("Entering {}".format(FUNCTION_NAME))
51 |
52 | user_id = token_data["$id"]
53 |
54 | api_key_response = api_gateway.create_api_key(name=str(uuid.uuid4()), enabled=True)
55 |
56 | api_key = api_key_response["value"]
57 | api_key_id = api_key_response["id"]
58 | usage_plan_name = "ByteShareDevUsagePlan"
59 |
60 | usage_plan_response = api_gateway.get_usage_plans()
61 | usage_plans = usage_plan_response["items"]
62 |
63 | for plan in usage_plans:
64 | if plan["name"] == usage_plan_name:
65 | usage_plan_id = plan["id"]
66 | break
67 |
68 | # TODO: Remove this
69 | # if os.getenv("ENVIRONMENT") == "dev" and not found_usage_plan:
70 | # usage_plan_response = api_gateway.create_usage_plan(
71 | # name="ByteShareDevUsagePlan",
72 | # throttle={"burstLimit": 5, "rateLimit": 10},
73 | # quota={"limit": 20, "offset": 0, "period": "DAY"},
74 | # )
75 |
76 | # usage_plan_id = usage_plan_response["id"]
77 |
78 | api_gateway.create_usage_plan_key(
79 | usagePlanId=usage_plan_id, keyId=api_key_id, keyType="API_KEY"
80 | )
81 | time_now = datetime.now(timezone.utc)
82 |
83 | apikey_metadata = apikey_dynamodb.read_item({"user_id": user_id})
84 | if not apikey_metadata:
85 | apikey_metadata = {
86 | "user_id": user_id,
87 | "apikey": api_key,
88 | "apikey_id": api_key_id,
89 | "used_per_apikey": 0,
90 | "total_used": 0,
91 | "last_used_per_apikey": "",
92 | "updated_at": "",
93 | "created_at": time_now.isoformat(),
94 | }
95 | apikey_dynamodb.create_item(apikey_metadata)
96 | else:
97 | api_gateway.delete_api_key(apiKey=apikey_metadata["apikey_id"])
98 |
99 | keys = {"user_id": user_id}
100 | update_data = {
101 | "apikey": api_key,
102 | "apikey_id": api_key_id,
103 | "last_used_per_apikey": "",
104 | "used_per_apikey": 0,
105 | "updated_at": time_now.isoformat(),
106 | }
107 | apikey_dynamodb.update_item(keys, update_data)
108 |
109 | log.info("Exiting {}".format(FUNCTION_NAME))
110 | return {"api_key": api_key}
111 |
112 |
113 | def delete_apikey_return_done(token_data):
114 | FUNCTION_NAME = "delete_apikey_return_done()"
115 | log.info("Entering {}".format(FUNCTION_NAME))
116 |
117 | user_id = token_data["$id"]
118 |
119 | apikey_metadata = apikey_dynamodb.read_item({"user_id": user_id})
120 | if not apikey_metadata:
121 | raise HTTPException(status_code=400, detail="No API key found")
122 | else:
123 | api_gateway.delete_api_key(apiKey=apikey_metadata["apikey_id"])
124 | apikey_dynamodb.delete_item({"user_id": user_id})
125 |
126 | log.info("Exiting {}".format(FUNCTION_NAME))
127 | return {"status": "Done"}
128 |
--------------------------------------------------------------------------------
/ui/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 | const svgToDataUri = require("mini-svg-data-uri");
3 | const {
4 | default: flattenColorPalette,
5 | } = require("tailwindcss/lib/util/flattenColorPalette");
6 | const config = {
7 | darkMode: ['class'],
8 | content: [
9 | './pages/**/*.{ts,tsx}',
10 | './components/**/*.{ts,tsx}',
11 | './app/**/*.{ts,tsx}',
12 | './src/**/*.{ts,tsx}',
13 | ],
14 | prefix: '',
15 | theme: {
16 | fontSize: {
17 | xxs: '0.6rem',
18 | xs: '0.75rem',
19 | sm: '0.875rem',
20 | base: '1rem',
21 | lg: '1.125rem',
22 | xl: '1.25rem',
23 | '2xl': '1.5rem',
24 | '3xl': '1.875rem',
25 | '4xl': '2.25rem',
26 | '5xl': '3rem',
27 | '6xl': '4rem',
28 | },
29 | container: {
30 | center: true,
31 | padding: '2rem',
32 | screens: {
33 | '2xl': '1400px',
34 | },
35 | },
36 | extend: {
37 | backgroundColor: {
38 | main: '#303FA4',
39 | },
40 |
41 | colors: {
42 | border: 'hsl(var(--border))',
43 | input: 'hsl(var(--input))',
44 | ring: 'hsl(var(--ring))',
45 | background: 'hsl(var(--background))',
46 | foreground: 'hsl(var(--foreground))',
47 | primary: {
48 | DEFAULT: 'hsl(var(--primary))',
49 | foreground: 'hsl(var(--primary-foreground))',
50 | },
51 |
52 | darkbg:{
53 | DEFAULT:'var(--dark-blue)',
54 | foreground:'var(--light-dark-blue)'
55 | },
56 | purple:{
57 | DEFAULT:'var(--grape)',
58 | foreground:'var(--grape-violet)'
59 | },
60 | lightgray:{
61 | DEFAULT: 'hsl(var(--lightgray))',
62 | foreground: 'hsl(var(--lightgray-foreground))',
63 | },
64 | secondary: {
65 | DEFAULT: 'hsl(var(--secondary))',
66 | foreground: 'hsl(var(--secondary-foreground))',
67 | },
68 | destructive: {
69 | DEFAULT: 'hsl(var(--destructive))',
70 | foreground: 'hsl(var(--destructive-foreground))',
71 | },
72 | muted: {
73 | DEFAULT: 'hsl(var(--muted))',
74 | foreground: 'hsl(var(--muted-foreground))',
75 | },
76 | accent: {
77 | DEFAULT: 'hsl(var(--accent))',
78 | foreground: 'hsl(var(--accent-foreground))',
79 | },
80 | popover: {
81 | DEFAULT: 'hsl(var(--popover))',
82 | foreground: 'hsl(var(--popover-foreground))',
83 | },
84 | card: {
85 | DEFAULT: 'hsl(var(--card))',
86 | foreground: 'hsl(var(--card-foreground))',
87 | },
88 | },
89 | borderRadius: {
90 | lg: 'var(--radius)',
91 | md: 'calc(var(--radius) - 2px)',
92 | sm: 'calc(var(--radius) - 4px)',
93 | },
94 | keyframes: {
95 | 'accordion-down': {
96 | from: { height: '0' },
97 | to: { height: 'var(--radix-accordion-content-height)' },
98 | },
99 | 'accordion-up': {
100 | from: { height: 'var(--radix-accordion-content-height)' },
101 | to: { height: '0' },
102 | },
103 | heartbeat: {
104 | '0%, 100%': { transform: 'scale(1)' },
105 | '50%': { transform: 'scale(1.2)' },
106 | },
107 | bounce: {
108 | '0%, 100%': { transform: 'translateY(0)' },
109 | '50%': { transform: 'translateY(-5px)' },
110 | },
111 | },
112 | animation: {
113 | 'accordion-down': 'accordion-down 0.2s ease-out',
114 | 'accordion-up': 'accordion-up 0.2s ease-out',
115 | heartbeat: 'heartbeat 1.5s ease-in-out infinite',
116 | bounce: 'bounce 1.4s ease-in-out infinite',
117 |
118 |
119 | },
120 | },
121 | },
122 | plugins: [require('tailwindcss-animate'), addVariablesForColors,
123 | function ({ matchUtilities, theme }: any) {
124 | matchUtilities(
125 | {
126 | "bg-dot-thick": (value: any) => ({
127 | backgroundImage: `url("${svgToDataUri(
128 | ` `
129 | )}")`,
130 | }),
131 | },
132 | { values: flattenColorPalette(theme("backgroundColor")), type: "color" }
133 | );
134 | },],
135 | } satisfies Config
136 |
137 | function addVariablesForColors({ addBase, theme }: any) {
138 | let allColors = flattenColorPalette(theme("colors"));
139 | let newVars = Object.fromEntries(
140 | Object.entries(allColors).map(([key, val]) => [`--${key}`, val])
141 | );
142 |
143 | addBase({
144 | ":root": newVars,
145 | });
146 | }
147 |
148 | export default config
149 |
--------------------------------------------------------------------------------
/middleware/app/api/services/download.py:
--------------------------------------------------------------------------------
1 | import concurrent.futures
2 | from datetime import datetime, timezone
3 | from enum import Enum as PythonEnum
4 |
5 | import utils.helper as helper
6 | import utils.logger as logger
7 | from database.db import DynamoDBManager
8 | from fastapi import HTTPException
9 | from storage.cloudflare_r2 import CloudflareR2Manager
10 |
11 | # Logger instance
12 | log = logger.get_logger()
13 |
14 | # Storage
15 | BUCKET_NAME = "byteshare-blob"
16 | storage = CloudflareR2Manager(BUCKET_NAME)
17 |
18 | # DynamoDB
19 | table_name = "byteshare-upload-metadata"
20 | dynamodb = DynamoDBManager(table_name)
21 |
22 |
23 | class StatusEnum(PythonEnum):
24 | initiated = "initiated"
25 | uploaded = "uploaded"
26 |
27 |
28 | def get_file_url_return_name_link(token_data, upload_id: str):
29 | FUNCTION_NAME = "get_file_url_return_name_link()"
30 | log.info("Entering {}".format(FUNCTION_NAME))
31 |
32 | file_data = {}
33 | time_now = datetime.now(timezone.utc).isoformat()
34 | upload_metadata = dynamodb.read_item({"upload_id": upload_id})
35 | if upload_metadata == None:
36 | log.warning(
37 | "BAD REQUEST for UploadID: {} ERROR: {}".format(
38 | upload_id, "Upload ID not valid."
39 | )
40 | )
41 | raise HTTPException(status_code=400, detail="Upload ID not valid")
42 | if upload_metadata["status"] != StatusEnum.uploaded.name:
43 | log.warning(
44 | "BAD REQUEST for UploadID: {} ERROR: {}".format(
45 | upload_id, "Incomplete upload."
46 | )
47 | )
48 | raise HTTPException(status_code=400, detail="Incomplete upload")
49 |
50 | expires_at = upload_metadata["expires_at"]
51 | if time_now > expires_at:
52 | log.warning(
53 | "BAD REQUEST for UploadID: {} ERROR: {}".format(
54 | upload_id, "Link is expired."
55 | )
56 | )
57 | raise HTTPException(status_code=400, detail="Link is expired")
58 |
59 | download_count = upload_metadata["download_count"]
60 | max_count = upload_metadata["max_download"]
61 | if token_data == None or upload_metadata["creator_id"] != token_data["$id"]:
62 | if download_count >= max_count:
63 | log.warning(
64 | "BAD REQUEST for UploadID: {} ERROR: {}".format(
65 | upload_id, "Download limit exceeded"
66 | )
67 | )
68 | raise HTTPException(status_code=400, detail="Download limit exceeded")
69 |
70 | file_names = list(upload_metadata["storage_file_metadata"].keys())
71 |
72 | with concurrent.futures.ThreadPoolExecutor(max_workers=25) as executor:
73 | future_to_file_name = {
74 | executor.submit(_generate_download_url, upload_id, file_name): file_name
75 | for file_name in file_names
76 | }
77 | for future in concurrent.futures.as_completed(future_to_file_name):
78 | file_name = future_to_file_name[future]
79 | try:
80 | response = future.result()
81 | # file_format, file_size, file_url = future.result()
82 |
83 | if upload_metadata["share_email_as_source"]:
84 | file_data["user_email"] = upload_metadata["creator_email"]
85 | else:
86 | file_data["user_email"] = None
87 | file_data[file_name] = {}
88 | file_data[file_name]["format"] = response["file_format"]
89 | file_data[file_name]["size"] = helper.format_size(response["file_size"])
90 | file_data[file_name]["download_url"] = response["file_url"]
91 | except Exception as e:
92 | log.error(
93 | "EXCEPTION occurred for Upload ID: {}\nFile {}: \nERROR:{}".format(
94 | upload_id, file_name, str(e)
95 | )
96 | )
97 |
98 | if token_data == None or upload_metadata["creator_id"] != token_data["$id"]:
99 | keys = {"upload_id": upload_id}
100 | update_data = {
101 | "download_count": download_count + 1,
102 | "updated_at": time_now,
103 | }
104 | dynamodb.update_item(keys, update_data)
105 |
106 | log.info("Exiting {}".format(FUNCTION_NAME))
107 | return file_data
108 |
109 |
110 | def _generate_download_url(upload_id, file_name):
111 | FUNCTION_NAME = "_generate_download_url()"
112 | log.info("Entering {}".format(FUNCTION_NAME))
113 |
114 | file_path = upload_id + "/" + file_name
115 |
116 | file_format = helper.get_file_extension(file_name)
117 | file_size = storage.get_file_info(file_path)
118 |
119 | download_expiration_time = 21600 # 6 hours
120 |
121 | # Generate share download link
122 | file_url = storage.generate_download_url(file_path, download_expiration_time)
123 |
124 | log.info("Exiting {}".format(FUNCTION_NAME))
125 |
126 | return {"file_format": file_format, "file_size": file_size, "file_url": file_url}
127 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | # Contributing to ByteShare.io
15 | Welcome to ByteShare! ❤️
16 |
17 | Your contributions are invaluable to our open-source community. Whether you're a seasoned developer or a newcomer, your input is greatly appreciated. Thank you for considering contributing. **Let's build something amazing together**!
18 |
19 | ## How to get started?
20 | - Before creating a new issue or PR, check if that [Issues](https://github.com/innovencelabs/byteshare/issues) or [PRs](https://github.com/innovencelabs/byteshare/pulls) already exists.
21 | - Prerequisites
22 | - Docker
23 | - Terraform
24 | - AWS CLI configured with your AWS account (to create resources)
25 | - [Cloudflare account](https://www.cloudflare.com/)
26 | - [Appwrite account](https://appwrite.io/)
27 | - Steps to run locally
28 | 1. Fork this repository to your Github account and clone to local machine using **git clone**.
29 | 2. Take a pull from the origin
30 | ```bash
31 | git pull
32 | ```
33 | 3. Create the branch from *master* after creating the Issue like: ***ISSUE_ID-DESCRIPTION***
34 | ```bash
35 | git checkout -b [new_branch_name]
36 | ```
37 | 4. Create resources using terraform
38 | ```bash
39 | cd ByteShare/infrastructure/cloud
40 | cp terraform.tfvars.example terraform.tfvars
41 | # Add your credentials in terraform.tfvars
42 | terraform init
43 | terraform plan
44 | terraform apply
45 | cd ../..
46 | ```
47 | 5. Create your Organisation and Project in Appwrite [TUTORIAL](https://youtu.be/pk92hS_d_ns?t=11&si=emSqp8Mdra_iF-dc)
48 | 6. Add the credential values in .env
49 | ```bash
50 | cp .env.example .env
51 | # Add your credentials in .env
52 | ```
53 | 7. Run the application through Docker Compose
54 | ```bash
55 | docker compose up --build
56 | ```
57 |
58 | #### By default, UI runs on port **3000** and Middleware runs on port **8000**
59 |
60 | ## Submit a Pull Request
61 | Always create an Issue before making any contribution.
62 | Branch can be created from Issue page directly as per the convention
63 |
64 | Branch naming convention:
65 | ***ISSUE_ID-DESCRIPTION***
66 |
67 | Example:
68 | ```
69 | 106-add-password-support-for-sharing-files
70 | ```
71 |
72 | **For creating a pull request**:
73 | - After you are done with your changes, push the code and create a pull request for master of the ByteShare repository.
74 | - Get the code reviewed, approved and merged to ***master*** branch.
75 | - Once merged, the CI/CD pipelines will be automatically triggered and the changes will be reflected in the app.
76 |
77 | Instructions:
78 | - **All pull requests must include a commit message detailing the changes made.**
79 | - Always take a pull from origin before making any new branch and pushing the code
80 | ```bash
81 | git pull
82 | ```
83 |
84 | ## File structure
85 | ```
86 | ├── ui # Frontend application
87 | │ └── src # Source code
88 | │ ├── app
89 | │ ├── authentication
90 | │ ├── components
91 | │ ├── conf
92 | │ ├── context
93 | │ └── lib
94 | ├── middeware # Backend application
95 | │ └── app # Source code
96 | │ ├── api # APIs
97 | │ │ ├── routes # API endpoint routes
98 | │ │ │ ├── access # Secured API access endpoints routes
99 | │ │ │ ├── secured # Secured API endpoints routes
100 | │ │ │ └── webhook # Webhook API endpoints routes
101 | │ │ └── services # API service code
102 | │ │ ├── access # Secured API access service code
103 | │ │ ├── secured # Secured API service code
104 | │ │ └── webhook # Webhook API service code
105 | │ ├── database # Database code
106 | │ ├── storage # Storage code
107 | │ └── utils # Utility code
108 | └── infrastructure # Terraform scripts for cloud and local(docker version)
109 | ├── cloud # For clouds
110 | └── local # For docker containers
111 |
112 | ```
113 |
114 | ## Architecture
115 | 
116 |
117 | ## Built with
118 | - NextJS
119 | - ShadCN (TailwindCSS)
120 | - FastAPI
121 | - AWS DynamoDB
122 | - Cloudflare R2
123 | - AWS Cloudwatch
124 | - Appwrite
125 | - Resend
126 |
127 |
--------------------------------------------------------------------------------
/infrastructure/cloud/main.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | aws = {
4 | source = "hashicorp/aws"
5 | version = "~> 5"
6 | }
7 | }
8 | }
9 |
10 | variable "r2_access_key" {}
11 | variable "r2_secret_key" {}
12 | variable "r2_account_id" {}
13 |
14 | provider "aws" {
15 | alias = "aws"
16 | region = "us-east-2"
17 | }
18 |
19 | provider "aws" {
20 | alias = "r2"
21 | region = "auto"
22 |
23 | access_key = var.r2_access_key
24 | secret_key = var.r2_secret_key
25 |
26 | skip_credentials_validation = true
27 | skip_region_validation = true
28 | skip_requesting_account_id = true
29 |
30 | endpoints {
31 | s3 = "https://${var.r2_account_id}.r2.cloudflarestorage.com"
32 | }
33 | }
34 |
35 |
36 | # Bucket
37 | resource "aws_s3_bucket" "byteshare-blob" {
38 | provider = aws.r2
39 | bucket = "byteshare-blob"
40 | }
41 |
42 | resource "aws_s3_bucket_lifecycle_configuration" "expire-object" {
43 | provider = aws.r2
44 | bucket = aws_s3_bucket.byteshare-blob.id
45 |
46 | rule {
47 | id = "expire_object"
48 | status = "Enabled"
49 | expiration {
50 | days = 60
51 | }
52 | }
53 | }
54 |
55 | resource "aws_s3_bucket_cors_configuration" "allow-cors" {
56 | provider = aws.r2
57 | bucket = aws_s3_bucket.byteshare-blob.id
58 |
59 | cors_rule {
60 | allowed_headers = ["*"]
61 | allowed_methods = ["PUT", "GET", "HEAD"]
62 | allowed_origins = ["*"]
63 | expose_headers = ["Content-Length", "Content-Type"]
64 | }
65 | }
66 |
67 | # DynamoDB table
68 | resource "aws_dynamodb_table" "byteshare-upload-metadata" {
69 | provider = aws.aws
70 | name = "byteshare-upload-metadata"
71 | billing_mode = "PAY_PER_REQUEST"
72 | hash_key = "upload_id"
73 |
74 | attribute {
75 | name = "upload_id"
76 | type = "S"
77 | }
78 |
79 | attribute {
80 | name = "creator_id"
81 | type = "S"
82 | }
83 |
84 | global_secondary_index {
85 | name = "userid-gsi"
86 | hash_key = "creator_id"
87 | projection_type = "ALL"
88 | }
89 | }
90 |
91 | resource "aws_dynamodb_table" "byteshare-user" {
92 | provider = aws.aws
93 | name = "byteshare-user"
94 | billing_mode = "PAY_PER_REQUEST"
95 | hash_key = "user_id"
96 |
97 | attribute {
98 | name = "user_id"
99 | type = "S"
100 | }
101 | }
102 |
103 | resource "aws_dynamodb_table" "byteshare-feedback" {
104 | provider = aws.aws
105 | name = "byteshare-feedback"
106 | billing_mode = "PAY_PER_REQUEST"
107 | hash_key = "feedback_id"
108 |
109 | attribute {
110 | name = "feedback_id"
111 | type = "S"
112 | }
113 | }
114 |
115 | resource "aws_dynamodb_table" "byteshare-subscriber" {
116 | provider = aws.aws
117 | name = "byteshare-subscriber"
118 | billing_mode = "PAY_PER_REQUEST"
119 | hash_key = "email"
120 | range_key = "created_at"
121 |
122 | attribute {
123 | name = "email"
124 | type = "S"
125 | }
126 | attribute {
127 | name = "created_at"
128 | type = "S"
129 | }
130 | }
131 |
132 |
133 | resource "aws_dynamodb_table" "byteshare-apikey" {
134 | provider = aws.aws
135 | name = "byteshare-apikey"
136 | billing_mode = "PAY_PER_REQUEST"
137 | hash_key = "user_id"
138 |
139 | attribute {
140 | name = "user_id"
141 | type = "S"
142 | }
143 | attribute {
144 | name = "apikey"
145 | type = "S"
146 | }
147 |
148 |
149 | global_secondary_index {
150 | name = "apikey-gsi"
151 | hash_key = "apikey"
152 | projection_type = "ALL"
153 | }
154 | }
155 |
156 |
157 | resource "aws_iam_role" "api_gateway_invoke_role" {
158 | provider = aws.aws
159 | name = "ByteShareAPIInvokeRole"
160 | assume_role_policy = jsonencode({
161 | "Version" : "2012-10-17",
162 | "Statement" : [
163 | {
164 | "Effect" : "Allow",
165 | "Principal" : {
166 | "AWS" : aws_iam_user.unprivileged_user.arn
167 | },
168 | "Action" : "sts:AssumeRole"
169 | }
170 | ]
171 | })
172 | }
173 |
174 | resource "aws_iam_policy_attachment" "api_gateway_invoke_policy_attachment" {
175 | provider = aws.aws
176 | name = "api_gateway_invoke_policy_attachment"
177 | policy_arn = "arn:aws:iam::aws:policy/AmazonAPIGatewayInvokeFullAccess"
178 | roles = [aws_iam_role.api_gateway_invoke_role.name]
179 | }
180 |
181 | resource "aws_iam_user" "unprivileged_user" {
182 | provider = aws.aws
183 | name = "byteshare-ui"
184 | }
185 |
186 | resource "aws_iam_policy" "assume_role_policy" {
187 | provider = aws.aws
188 | name = "assume_role_policy"
189 | description = "Allows user to assume the role"
190 | policy = jsonencode({
191 | "Version" : "2012-10-17",
192 | "Statement" : [
193 | {
194 | "Effect" : "Allow",
195 | "Action" : "sts:AssumeRole",
196 | "Resource" : aws_iam_role.api_gateway_invoke_role.arn
197 | }
198 | ]
199 | })
200 | }
201 |
202 | resource "aws_iam_user_policy_attachment" "assume_role_policy_attachment" {
203 | provider = aws.aws
204 | user = aws_iam_user.unprivileged_user.name
205 | policy_arn = aws_iam_policy.assume_role_policy.arn
206 | }
--------------------------------------------------------------------------------
/ui/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5 | import { VariantProps } from 'class-variance-authority'
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ))
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | )
60 | AlertDialogHeader.displayName = "AlertDialogHeader"
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | )
74 | AlertDialogFooter.displayName = "AlertDialogFooter"
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ))
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | {
104 | variant?: VariantProps['variant']
105 | } & React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
116 | ))
117 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
118 |
119 | const AlertDialogCancel = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, ...props }, ref) => (
123 |
132 | ))
133 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
134 |
135 | export {
136 | AlertDialog,
137 | AlertDialogPortal,
138 | AlertDialogOverlay,
139 | AlertDialogTrigger,
140 | AlertDialogContent,
141 | AlertDialogHeader,
142 | AlertDialogFooter,
143 | AlertDialogTitle,
144 | AlertDialogDescription,
145 | AlertDialogAction,
146 | AlertDialogCancel,
147 | }
148 |
--------------------------------------------------------------------------------
/middleware/app/storage/cloudflare_r2.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import boto3
4 | import utils.logger as logger
5 | from dotenv import load_dotenv
6 | from fastapi import HTTPException
7 | from storage.storage import BaseStorage
8 |
9 | load_dotenv()
10 |
11 | # Logger instance
12 | log = logger.get_logger()
13 |
14 |
15 | class CloudflareR2Manager(BaseStorage):
16 | def __init__(self, bucket_name: str):
17 | endpoint_url = f"https://{os.getenv('R2_ACCOUNT_ID')}.r2.cloudflarestorage.com"
18 | self.bucket_name = bucket_name
19 | self.r2 = boto3.client(
20 | "s3",
21 | endpoint_url=endpoint_url,
22 | aws_access_key_id=os.getenv("R2_ACCESS_KEY"),
23 | aws_secret_access_key=os.getenv("R2_SECRET_KEY"),
24 | region_name="auto",
25 | )
26 |
27 | def health_check(self):
28 | FUNCTION_NAME = "health_check()"
29 | log.info("Entering {}".format(FUNCTION_NAME))
30 |
31 | try:
32 | response = self.r2.list_objects_v2(Bucket=self.bucket_name)
33 | except Exception as e:
34 | log.error("EXCEPTION occurred connecting to R2.\nERROR: {}".format(str(e)))
35 | raise HTTPException(status_code=503, detail="R2 connection failed")
36 |
37 | log.info("Exiting {}".format(FUNCTION_NAME))
38 |
39 | def generate_upload_url(self, file_path: str, expirations_seconds: int):
40 | FUNCTION_NAME = "generate_upload_url()"
41 | log.info("Entering {}".format(FUNCTION_NAME))
42 |
43 | try:
44 | log.info("Exiting {}".format(FUNCTION_NAME))
45 | return self.r2.generate_presigned_url(
46 | "put_object",
47 | Params={"Bucket": self.bucket_name, "Key": file_path},
48 | ExpiresIn=expirations_seconds,
49 | HttpMethod="PUT",
50 | )
51 | except Exception as e:
52 | log.error(
53 | "EXCEPTION occurred genering upload url to R2.\nERROR: {}".format(
54 | str(e)
55 | )
56 | )
57 | raise HTTPException(status_code=500, detail=str(e))
58 |
59 | def generate_download_url(self, file_path: str, expirations_seconds: int):
60 | FUNCTION_NAME = "generate_download_url()"
61 | log.info("Entering {}".format(FUNCTION_NAME))
62 |
63 | try:
64 | log.info("Exiting {}".format(FUNCTION_NAME))
65 | return self.r2.generate_presigned_url(
66 | "get_object",
67 | Params={"Bucket": self.bucket_name, "Key": file_path},
68 | ExpiresIn=expirations_seconds,
69 | )
70 | except Exception as e:
71 | log.error(
72 | "EXCEPTION occurred genering download url to R2.\nERROR: {}".format(
73 | str(e)
74 | )
75 | )
76 | raise HTTPException(status_code=500, detail=str(e))
77 |
78 | def upload_file(self, localpath: str, file_path: str):
79 | FUNCTION_NAME = "upload_file()"
80 | log.info("Entering {}".format(FUNCTION_NAME))
81 |
82 | try:
83 | self.r2.upload_file(localpath, self.bucket_name, file_path)
84 | except Exception as e:
85 | log.error(
86 | "EXCEPTION occurred uploading file to R2.\nERROR: {}".format(str(e))
87 | )
88 | raise HTTPException(status_code=500, detail=str(e))
89 |
90 | log.info("Exiting {}".format(FUNCTION_NAME))
91 |
92 | def is_file_present(self, file_path: str):
93 | FUNCTION_NAME = "is_file_present()"
94 | log.info("Entering {}".format(FUNCTION_NAME))
95 |
96 | try:
97 | response = self.r2.head_object(Bucket=self.bucket_name, Key=file_path)
98 | except self.r2.exceptions.ClientError as e:
99 | if e.response["Error"]["Code"] == "404":
100 | log.warning(
101 | "BAD REQUEST file does not exists: {} in Bucket: {}".format(
102 | file_path, self.bucket_name
103 | )
104 | )
105 | return False
106 | else:
107 | log.error(
108 | "EXCEPTION occurred in checking availability in R2.\nERROR: {}".format(
109 | str(e)
110 | )
111 | )
112 | raise HTTPException(status_code=500, detail=str(e))
113 |
114 | log.info("Exiting {}".format(FUNCTION_NAME))
115 | return True
116 |
117 | def get_file_info(self, file_path: str):
118 | FUNCTION_NAME = "get_file_info()"
119 | log.info("Entering {}".format(FUNCTION_NAME))
120 |
121 | try:
122 | response = self.r2.head_object(Bucket=self.bucket_name, Key=file_path)
123 |
124 | file_size = response["ContentLength"]
125 |
126 | log.info("Exiting {}".format(FUNCTION_NAME))
127 | return file_size
128 | except Exception as e:
129 | log.error(
130 | "EXCEPTION occurred in getting file info in R2.\nERROR: {}".format(
131 | str(e)
132 | )
133 | )
134 | raise HTTPException(status_code=500, detail=str(e))
135 |
136 | def _get_exact_format(self, file_format):
137 | return file_format.split("/")[-1]
138 |
--------------------------------------------------------------------------------
/ui/src/components/ui/navigation-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { ChevronDownIcon } from '@radix-ui/react-icons'
3 | import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu'
4 | import { cva } from 'class-variance-authority'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const NavigationMenu = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
20 | {children}
21 |
22 |
23 | ))
24 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
25 |
26 | const NavigationMenuList = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ))
39 | NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
40 |
41 | const NavigationMenuItem = NavigationMenuPrimitive.Item
42 |
43 | const navigationMenuTriggerStyle = cva(
44 | 'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50',
45 | )
46 |
47 | const NavigationMenuTrigger = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, children, ...props }, ref) => (
51 |
56 | {children}{' '}
57 |
61 |
62 | ))
63 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
64 |
65 | const NavigationMenuContent = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
77 | ))
78 | NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
79 |
80 | const NavigationMenuLink = NavigationMenuPrimitive.Link
81 |
82 | const NavigationMenuViewport = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
87 |
95 |
96 | ))
97 | NavigationMenuViewport.displayName =
98 | NavigationMenuPrimitive.Viewport.displayName
99 |
100 | const NavigationMenuIndicator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
112 |
113 |
114 | ))
115 | NavigationMenuIndicator.displayName =
116 | NavigationMenuPrimitive.Indicator.displayName
117 |
118 | export {
119 | navigationMenuTriggerStyle,
120 | NavigationMenu,
121 | NavigationMenuList,
122 | NavigationMenuItem,
123 | NavigationMenuContent,
124 | NavigationMenuTrigger,
125 | NavigationMenuLink,
126 | NavigationMenuIndicator,
127 | NavigationMenuViewport,
128 | }
129 |
--------------------------------------------------------------------------------
/middleware/app/database/db.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import boto3
4 | import utils.logger as logger
5 | from fastapi import HTTPException
6 |
7 | # Logger instance
8 | log = logger.get_logger()
9 |
10 |
11 | class DynamoDBManager:
12 | def __init__(self, table_name):
13 | self.table_name = table_name
14 | if os.getenv("ENVIRONMENT") == "production":
15 | self.dynamodb = boto3.resource(
16 | "dynamodb",
17 | region_name=os.getenv("AWS_APP_REGION"),
18 | )
19 | else:
20 | self.dynamodb = boto3.resource(
21 | "dynamodb",
22 | aws_access_key_id=os.getenv("AWS_APP_ACCESS_KEY"),
23 | aws_secret_access_key=os.getenv("AWS_APP_SECRET_ACCESS_KEY"),
24 | region_name=os.getenv("AWS_APP_REGION"),
25 | )
26 | self.table = self.dynamodb.Table(table_name)
27 |
28 | def health_check(self):
29 | FUNCTION_NAME = "health_check()"
30 | log.info("Entering {}".format(FUNCTION_NAME))
31 |
32 | try:
33 | response = self.table.scan()
34 | if "Items" not in response:
35 | log.error(
36 | "EXCEPTION occurred connecting to DB. ERROR: {}".format(
37 | "Database connection failed."
38 | )
39 | )
40 | raise HTTPException(
41 | status_code=503, detail="Database connection failed"
42 | )
43 | except Exception as e:
44 | log.error("EXCEPTION occurred connecting to DB. ERROR: {}".format(str(e)))
45 | raise HTTPException(status_code=503, detail="Database connection failed")
46 |
47 | log.info("Exiting {}".format(FUNCTION_NAME))
48 |
49 | def create_item(self, item):
50 | FUNCTION_NAME = "create_item()"
51 | log.info("Entering {}".format(FUNCTION_NAME))
52 |
53 | try:
54 | response = self.table.put_item(Item=item)
55 | log.info("Added to DB.")
56 | except Exception as e:
57 | log.error(
58 | "EXCEPTION occurred adding new row to DB.\nItem: {} ERROR: {}".format(
59 | item, str(e)
60 | )
61 | )
62 | return
63 |
64 | log.info("Exiting {}".format(FUNCTION_NAME))
65 |
66 | def read_item(self, key):
67 | FUNCTION_NAME = "read_item()"
68 | log.info("Entering {}".format(FUNCTION_NAME))
69 |
70 | try:
71 | response = self.table.get_item(Key=key)
72 | item = response.get("Item")
73 | if item:
74 | log.info("Exiting {}".format(FUNCTION_NAME))
75 | return item
76 | else:
77 | log.warning(
78 | "BAD REQUEST for Key: {} ERROR: {}".format(key, "Item not found.")
79 | )
80 | return None
81 | except Exception as e:
82 | log.error(
83 | "EXCEPTION occurred in reading row to DB for Key:{} ERROR: {}".format(
84 | key, str(e)
85 | )
86 | )
87 |
88 | def read_items(self, key_name, key_value, index_name):
89 | FUNCTION_NAME = "read_items()"
90 | log.info("Entering {}".format(FUNCTION_NAME))
91 |
92 | try:
93 | response = self.table.query(
94 | IndexName=index_name,
95 | KeyConditionExpression=key_name + " = :key_value",
96 | ExpressionAttributeValues={":key_value": key_value},
97 | )
98 | items = response.get("Items", [])
99 | if items:
100 | log.info("Exiting {}".format(FUNCTION_NAME))
101 | return items
102 | else:
103 | log.warning(
104 | "BAD REQUEST for Key: {} ERROR: {}".format(
105 | key_name, "Item not found."
106 | )
107 | )
108 | return []
109 | except Exception as e:
110 | log.error(
111 | "EXCEPTION occurred in reading row to DB for Key:{} ERROR: {}".format(
112 | key_name, str(e)
113 | )
114 | )
115 | print(f"Error: {key_name}={key_value}\nError: {e}")
116 |
117 | def update_item(self, key, update_data):
118 | FUNCTION_NAME = "update_item()"
119 | log.info("Entering {}".format(FUNCTION_NAME))
120 |
121 | try:
122 | update_expression = "SET " + ", ".join(
123 | [f"#{field} = :{field}" for field in update_data.keys()]
124 | )
125 | expression_attribute_values = {
126 | f":{field}": value for field, value in update_data.items()
127 | }
128 | expression_attribute_names = {
129 | f"#{field}": field for field in update_data.keys()
130 | }
131 |
132 | response = self.table.update_item(
133 | Key=key,
134 | UpdateExpression=update_expression,
135 | ExpressionAttributeValues=expression_attribute_values,
136 | ExpressionAttributeNames=expression_attribute_names,
137 | ReturnValues="UPDATED_NEW",
138 | )
139 | log.info("Updated to DB.")
140 | except Exception as e:
141 | log.error(
142 | "EXCEPTION occurred in updating row to DB for Key:{}\nUpdate Data: {} ERROR: {}".format(
143 | key, update_data, str(e)
144 | )
145 | )
146 | log.info("Exiting {}".format(FUNCTION_NAME))
147 |
148 | def delete_item(self, key):
149 | FUNCTION_NAME = "delete_item()"
150 | log.info("Entering {}".format(FUNCTION_NAME))
151 |
152 | try:
153 | response = self.table.delete_item(Key=key)
154 | log.info("Deleted from DB.")
155 | except Exception as e:
156 | log.error(
157 | "EXCEPTION occurred in deleting row to DB for Key:{} ERROR: {}".format(
158 | key, str(e)
159 | )
160 | )
161 |
162 | log.info("Exiting {}".format(FUNCTION_NAME))
163 |
--------------------------------------------------------------------------------
/middleware/app/utils/auth.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import os
3 | from datetime import datetime, timezone
4 | from typing import Optional
5 |
6 | import utils.logger as logger
7 | from appwrite.client import Client
8 | from appwrite.services.account import Account
9 | from appwrite.services.users import Users
10 | from database.db import DynamoDBManager
11 | from dotenv import load_dotenv
12 | from fastapi import Header, HTTPException
13 |
14 | # Load Environment variables
15 | load_dotenv()
16 |
17 | # Logger instance
18 | log = logger.get_logger()
19 |
20 | # DynamoDB
21 | table_name = "byteshare-apikey"
22 | apikey_dynamodb = DynamoDBManager(table_name)
23 |
24 |
25 | def authenticate(
26 | x_auth_token: Optional[str] = Header(None), x_api_key: Optional[str] = Header(None)
27 | ):
28 | FUNCTION_NAME = "authenticate()"
29 | log.info("Entering {}".format(FUNCTION_NAME))
30 |
31 | if x_auth_token is None:
32 | raise HTTPException(
33 | status_code=401,
34 | detail="X-Auth-Token header is missing",
35 | headers={"WWW-Authenticate": "Bearer"},
36 | )
37 | if x_api_key != os.getenv("AWS_API_KEY"):
38 | raise HTTPException(
39 | status_code=403,
40 | detail="Given API Key not allowed",
41 | )
42 |
43 | try:
44 | token_type, token = x_auth_token.split()
45 | if token_type.lower() != "bearer":
46 | raise HTTPException(
47 | status_code=401,
48 | detail="Invalid token type",
49 | headers={"WWW-Authenticate": "Bearer"},
50 | )
51 | client = Client()
52 | (
53 | client.set_endpoint(os.getenv("APPWRITE_URL"))
54 | .set_project(os.getenv("APPWRITE_PROJECT_ID"))
55 | .set_jwt(token)
56 | )
57 |
58 | account = Account(client)
59 |
60 | log.info("Authenticated.")
61 | log.info("Exiting {}".format(FUNCTION_NAME))
62 | return account.get()
63 |
64 | except Exception as e:
65 | log.error("EXCEPTION authenticating: {}".format(str(e)))
66 | raise HTTPException(
67 | status_code=401,
68 | detail="Invalid token",
69 | headers={"WWW-Authenticate": "Bearer"},
70 | )
71 |
72 |
73 | def optional_authenticate(x_auth_token: Optional[str] = Header(None)):
74 | FUNCTION_NAME = "optional_authenticate()"
75 | log.info("Entering {}".format(FUNCTION_NAME))
76 |
77 | if x_auth_token is None:
78 | return None
79 |
80 | try:
81 | token_type, token = x_auth_token.split()
82 | if token_type.lower() != "bearer":
83 | return None
84 | client = Client()
85 | (
86 | client.set_endpoint(os.getenv("APPWRITE_URL"))
87 | .set_project(os.getenv("APPWRITE_PROJECT_ID"))
88 | .set_jwt(token)
89 | )
90 |
91 | account = Account(client)
92 |
93 | log.info("Authenticated.")
94 | log.info("Exiting {}".format(FUNCTION_NAME))
95 | return account.get()
96 |
97 | except Exception as e:
98 | log.error("EXCEPTION authenticating: {}".format(str(e)))
99 | return None
100 |
101 |
102 | def authenticate_appwrite_webhook(authorization: Optional[str] = Header(None)):
103 | if authorization == None:
104 | raise HTTPException(status_code=401, detail="Authorization header is missing")
105 |
106 | try:
107 | auth_type, encoded_credentials = authorization.split(" ")
108 | if auth_type.lower() != "basic":
109 | raise HTTPException(
110 | status_code=401, detail="Only Basic authentication is supported"
111 | )
112 |
113 | decoded_credentials = base64.b64decode(encoded_credentials).decode("utf-8")
114 | username, password = decoded_credentials.split(":")
115 |
116 | if username != os.getenv("APPWRITE_WEBHOOK_USER") or password != os.getenv(
117 | "APPWRITE_WEBHOOK_PASS"
118 | ):
119 | raise HTTPException(
120 | status_code=401, detail="Invalid authentication credentials"
121 | )
122 | except ValueError:
123 | raise HTTPException(
124 | status_code=401, detail="Invalid authorization header format"
125 | )
126 |
127 |
128 | def authenticate_scan(x_auth_token: Optional[str] = Header(None)):
129 | if x_auth_token == None:
130 | raise HTTPException(status_code=401, detail="X-Auth-Token header is missing")
131 |
132 | try:
133 | auth_type, encoded_credentials = x_auth_token.split(" ")
134 | if auth_type.lower() != "basic":
135 | raise HTTPException(
136 | status_code=401, detail="Only Basic authentication is supported"
137 | )
138 |
139 | decoded_credentials = base64.b64decode(encoded_credentials).decode("utf-8")
140 | username, password = decoded_credentials.split(":")
141 |
142 | if username != os.getenv("SCAN_USER") or password != os.getenv("SCAN_PASS"):
143 | raise HTTPException(
144 | status_code=401, detail="Invalid authentication credentials"
145 | )
146 | except ValueError:
147 | raise HTTPException(
148 | status_code=401, detail="Invalid X-Auth-Token header format"
149 | )
150 |
151 |
152 | def preprocess_external_call(api_key: str):
153 | if api_key == os.getenv("AWS_API_KEY"):
154 | raise HTTPException(
155 | status_code=401,
156 | detail="Given API Key not allowed",
157 | )
158 |
159 | client = Client()
160 | client.set_endpoint(os.getenv("APPWRITE_URL"))
161 | client.set_project(os.getenv("APPWRITE_PROJECT_ID"))
162 | client.set_key(os.getenv("APPWRITE_API_KEY"))
163 |
164 | users = Users(client)
165 |
166 | apikey_metadatas = apikey_dynamodb.read_items("apikey", api_key, "apikey-gsi")
167 |
168 | user_id = apikey_metadatas[0]["user_id"]
169 | used_per_apikey = apikey_metadatas[0]["used_per_apikey"]
170 | total_used = apikey_metadatas[0]["total_used"]
171 |
172 | result = users.get(user_id=user_id)
173 | time_now = datetime.now(timezone.utc)
174 |
175 | keys = {"user_id": user_id}
176 | update_data = {
177 | "used_per_apikey": used_per_apikey + 1,
178 | "total_used": total_used + 1,
179 | "last_used_per_apikey": time_now.isoformat(),
180 | }
181 | apikey_dynamodb.update_item(keys, update_data)
182 |
183 | return result
184 |
--------------------------------------------------------------------------------
/ui/src/app/(pages)/auth/login/components/auth-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import appwriteService from '@/authentication/appwrite/config'
4 | import { Icons } from '@/components/icons'
5 | import { Button } from '@/components/ui/button'
6 | import { Input } from '@/components/ui/input'
7 | import { Label } from '@/components/ui/label'
8 | import useAuth from '@/context/useAuth'
9 | import { cn } from '@/lib/utils'
10 | import { useRouter } from 'next/navigation'
11 | import {
12 | FormEvent,
13 | HTMLAttributes,
14 | useState
15 | } from 'react'
16 | import { toast } from 'sonner'
17 |
18 | interface UserAuthFormProps extends HTMLAttributes {}
19 |
20 | export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
21 | const router = useRouter()
22 | const { setAuthorised } = useAuth()
23 | const [formData, setFormData] = useState({
24 | email: '',
25 | password: '',
26 | })
27 | const [isLoading, setIsLoading] = useState(false)
28 | const { authorised, statusLoaded } = useAuth()
29 |
30 |
31 | const login = async (e: FormEvent) => {
32 | e.preventDefault()
33 | setIsLoading(true)
34 | try {
35 | const session = await appwriteService.login(formData)
36 | if (session) {
37 | setAuthorised(true)
38 | router.push('/')
39 | }
40 | } catch (err: any) {
41 | toast.error(err.message)
42 | }
43 |
44 | setIsLoading(false)
45 | }
46 |
47 | const loginWithGoogle = (e) => {
48 | e.preventDefault()
49 | setIsLoading(true)
50 |
51 | try {
52 | const successRedirect = process.env.NEXT_PUBLIC_APP_URL
53 | const failureRedirect = process.env.NEXT_PUBLIC_APP_URL + '/auth/login'
54 |
55 | appwriteService.loginWithGoogle({ successRedirect, failureRedirect })
56 | } catch (err) {
57 | toast.error(err.message)
58 | }
59 |
60 | setIsLoading(false)
61 | }
62 |
63 | return (
64 |
65 |
128 |
router.push('/auth/signup')}
132 | >
133 | Switch to Create Account
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 | Or continue with
142 |
143 |
144 |
145 |
151 | {isLoading ? (
152 |
153 | ) : (
154 |
161 |
165 |
169 |
173 |
177 |
178 | )}{' '}
179 | Continue with Google
180 |
181 |
182 | )
183 | }
184 |
--------------------------------------------------------------------------------