CloudSharePlus: Document Sharing App, Passage by 1Password Integration With Appwrite.
Table of contents
Introduction
CloudSharePlus is an innovative online document-sharing platform that eliminates the hassle of remembering passwords and empowers seamless collaboration. In this blog, I will take you through the inspiration, product details, challenges faced, and the incredible journey of bringing CloudSharePlus to life.
Inspiration
The integration of passwordless login with Supabase has served as a source of inspiration for me, it motivated me to explore the possibilities of integrating a similar solution with Appwrite as it provides a wide range of functionality, including user authentication, database administration, and file storage.
Integrating Backend as service applications like these with Passage would enhance user experience and mitigates password-related risks.
I came up with the idea to create a passwordless document-sharing platform that would enhance productivity and ease of use and cover most of the basic scenarios related to any Backend as service applications.
Team Members
I am proud to announce that I have completed the project entirely on my own.
CloudSharePlus Details
CloudSharePlus is a user-friendly online file sharing platform that leverages advanced authentication technologies to provide a seamless user experience. Here's a breakdown of the product
Authentication
Authentication is the simplest part of the application, CloudSharePlus doesn't have two different routes for registering or login, it has a single page using which a user can login, the only ask is an email address.
If the browser and the device supports then users can login using biometrics incase it is not supported a magic link is sent to the email address on click of which the user is logged in.
Dashboard
The dashboard comprises of 4 subsections, each one has been described below
Documents
This section consists of all the documents uploaded by the user, each document is shown as a card which has multiple buttons for multiple operations.
Recents
Recent lists all of the user's documents in descending order of their last modified dates, i.e if a user modifies any documents i.e make it favourite or shares it or vice versa then that will appear on top of the list in recents.Favourites
it lists all the documents which are marked favourite by the user by clicking on the hear button on the top right corner of the card.Trash
Documents which are deleted by the user by clicking on the trash button on the top left corner of the card appear here, notice that in this section a restore button appears instead of the trash button on the top left corner of the card, clicking on this will restore the document.
Deleted documents are not sharable
Upload View
Users can upload documents by clicking on the upload button on the right corner of the dashboard. On clicking a pop-up appears which has 2 fields, after uploading a file by clicking on the cloud button user can enter a custom name of the file and a description and hit upload to upload the file.
View Pop
Users can check each file by clicking on the view button on the card, it opens a popup showing the document name and description and a download button, clicking on the download will download the file.
Share File View
Users can share files with anyone using the global link, for that, the user needs to click on the share button on the card, and post that the shareable link is visible to the user which can be copied and shared across.
A person having the link need not be a CloudSharePlus user, following is the view of how a shared file would appear when someone tris to access using the link.
CloudSharePlus Architecture
Following is the high-level architecture diagram of the application.
Login is done on the client side by directly making a request to the passage server which returns a token which is sent with every request in the headers, this token is used by the backend to check if the user is authenticated or not if not the server throws 401 else the API logic is executed and a final response sent back to the client.
Could we have not avoided the backend by simply making requests to Appwrite from client?
The answer is no, Appwrite doesn't provide a custom RLS(Row Level Security) rule from any database or collection, so in order to execute any operations like file upload and database entries we would have to make the database open and accessible to all which is a disaster.
Hence backend is important as we can generate a key for our server and using that key server can perform operations on Appwrite, a very naive RLS is achieved by querying based on UID and the problem of public accessibility is also solved as the authentication middleware will only allow authenticated users to perform any operations.
Tech Stack
The development of CloudSharePlus required careful selection of a robust tech stack to ensure optimal performance, security, and scalability. Here's an overview of the technologies and frameworks used in building the platform.
VueJs for Frontend
Tailwind for Styling
Express for Backend
Passage by 1Password for Passwordless Authentication (User Management)
Appwrite for Database and Storage
Vercel for Backend Deployment
CloudflarePages for Frontend Deployment
Passage's Usage
Instead of going with passage elements, I went ahead with integration with passage.js for login or signup.
Consider the following UI
On click of the login button login function is called it checks if the user exists or not based on this result the decision is taken whether to register the user or login, and then it checks if the passage project is configured for webauthn and id webauthn is supported if so then login/registration using webauthn is initiated else login/register using the magic link is triggered.
In case of any failures, a fallback button to trigger the magic link appears for users to continute login via magic link.
import { Passage } from '@passageidentity/passage-js'
const passage = new Passage(import.meta.env.VITE_PASSAGE_KEY)
async login() {
// to reset any errors showing in the UI
this.resetErrors()
/* for loading spinner on the button and disbaling it
until the operation is completed*/
this.loginTriggered = true
const app = await passage.appInfo()
const webAuthConfig = passage.checkWebauthnConfig(app)
const isWebauthnSupported = await passage.isWebauthnSupported()
const userInfo = await passage.identifierExists(this.email)
let response
if (userInfo === null) {
//register
if (webAuthConfig && isWebauthnSupported) {
/* getResponseInDesiredFormat is used to handle errors,
basically a trycatch block, a description has been given in code block no 3*/
response = await this.getResponseInDesiredFormat(passage.register, this.email)
} else {
response = await this.getResponseInDesiredFormat(passage.newRegisterMagicLink, this.email)
}
} else {
//login
if (webAuthConfig && isWebauthnSupported) {
response = await this.getResponseInDesiredFormat(passage.login, this.email)
} else {
response = await this.getResponseInDesiredFormat(passage.newLoginMagicLink, this.email)
}
}
if (response.success) {
this.showSuccessText = true
if (webAuthConfig && isWebauthnSupported) {
this.successfulText = 'Completed the login using biometrics'
this.$router.push(response.data.redirect_url)
} else {
this.successfulText = "We've sent you a maigc link ,please check your email"
}
this.saveEmail()
} else {
this.showError = true
this.errorText = response.data.message
if (webAuthConfig && isWebauthnSupported) {
this.showSendMagicLinkButton = true
}
this.$router.replace({ query: null })
}
this.loginTriggered = false
}
In the case of the magic link, after clicking on it user is redirected to the same page but this time input and login button is disabled and on page load immediately authenticateMagicLink function is called.
authenticateMagicLink authenticates the magic link received using magicLinkActivate function provided by Passage, the code for the same is described below.
async authenticateMagicLink() {
this.validatingMagicLnk = true
let response
response = await this.getResponseInDesiredFormat(passage.magicLinkActivate, this.mlCode)
if (response.success) {
this.showSuccessText = true
this.successfulText = 'Validation Complete'
this.$router.push(response.data.redirect_url)
} else {
this.showError = true
this.errorText = response.data.message
this.$router.replace({ query: null })
}
this.validatingMagicLnk = false
}
getResponseInDesiredFormat is a helper fucntion to catch any errors and return response in a stnadard format.
async getResponseInDesiredFormat(fn, ...param) {
let response
try {
response = await fn.call(passage, ...param)
response = {
data: response,
success: true
}
} catch (error) {
response = {
data: error,
success: false
}
}
return response
},
Check this file for detailed code.
Appwrite'sUsage
Database and Storage were the services which I used from Appwrite.
Database
Used a single collection to store all the file-related details, but had to create multiple indexes. following is the schema of the table.
Collection - file_details
uid - String - passage user id
fid - String - bucket file id
file_name - String - custom name of the file given by user
is_deleted - Boolean - to toggle delete status
fav - Boolean - to toggle favourite status
is_shared - Boolean - to toggle share status
extension - String - extension of the file
description - String - custom description of the file
Only the Backend has the API Key with the permission to edit, read and create documents in the collection.
Storage
Used a single storage bucket for storing all the different files, on successful upload the file id is returned to the client via the backend which is used to make the entry in the file_details collection.
Again in this case too only the Backend has the API Key with the permission to create documents in the collection.
Challenges Faced
There were several difficulties encountered throughout the development of CloudSharePlus, some of which are detailed here.
The most challenging aspect of creating CloudSharePlus turned out to be the application's architectural design. had to think of a way of balancing the user-friendly interface, seamless authentication, and secure file storage.
I was initially thinking of not involving any backend and doing everything on the client but then realized soon that it would be impossible to do it just on the client end, a backend server was necessary, finally, I was able to come up with a solution which has been mentioned in the architecture section.Appwrites Cloud instance was quite slow and was not able to execute more than 3 queries even after indexing.
I was left with 2 choices either go for sorting on the backend or filtering based on a boolean and since sorting is more costly I went ahead with a custom filtering on the backend.
I don't exactly know about the problem but I strongly feel if I spin up a dedicated instance on cloud it might work as it was working locally, I didn't get the time to test the same but would test this out in a few days.
CloudSharePlus Code Repository
Here is the link to CloudSharePlus's code repository, feel free to raise issues and contribute.
https://github.com/baroiSubhra/CloudSharePlus
CloudSharePlus Link
Here is the link to CloudSharePlus
Site :- https://cloudshareplus.pages.dev
Email :- kuodnyg@mailto.plus
Name :- kuodnyg
To access the magic links navigate to this site and type the "Name" in the input box( watch the demo if it is unclear).
If It doesn't work then then you can login/register using any other email and start afresh.
CloudSharePlus Demo
Here is the link to the demo of CloudSharePlus
Conclusion
The journey of building CloudSharePlus has been an incredible learning experience.
I am happy as I was able to integrate Passage with Appwrite, with a sense of pride in my accomplishments, I am excited to present CloudSharePlus as my Hackathon submission.
I would love to get input from the community and would ask everyone to give it a go and raise your issues here.
I am grateful to Hashnode and 1Password for this opportunity to present my creation and share it with the world.
Subscribe to my newsletter
Read articles from Subhra Jyoti Baroi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Subhra Jyoti Baroi
Subhra Jyoti Baroi
I'm skilled fullstack developer with a passion for clean code and problem-solving, I have over 3 years of Industry experience with proficiency in various frontend frameworks and server side languages. In order to create cutting-edge solutions, I'm committed to remaining up-to-date with the most recent business trends and technology.