Hi Everyone. Very first blog here. Before reading the gibberish which I wrote here, give ClipBin a try at https://Clipb.in also feel free to play with its code at the ClipBin GitHub Repo
Well it started when I was brainstorming for CS50's final project. I thought why not make something like Pastebin. Like a website not for something fishy but for saving your clipboard items. Hence, I started calling it ClipBin (Clipboard + Pastebin). Without giving it a second thought, I took my laptop and started building the basic structure.
Here is what i drew...
So. Clearly this was not so amazing schema but it was working fine for me at that time. I choose SQLite as my goto database as it is lightweight, fast, secure and as I was working on a class project and not a big industry software, I decided to keep it simple. Below is the schema for the database i designed.
CREATE TABLE 'clips' (
"id" INTEGER NOT NULL UNIQUE,
"clip_url" TEXT NOT NULL UNIQUE,
"clip_name" TEXT,
"clip_text" TEXT, 'clip_time' DATETIME DEFAULT CURRENT_TIMESTAMP ,
PRIMARY KEY("id" AUTOINCREMENT)
)
Initially I started with the backend. Flask was my goto choice for the development as Flask is damn easy. I used the CS50's Standard Python Module for the connecting to the SQLite database. After building a basic structure, the codebase started to look something like this.
from cs50 import SQL
from flask import Flask
app = Flask(__name__)
db = SQL("sqlite:///clipbin.db")
The First route renders the index.html page. However, when user entered the data on the form, which ofcourse was on the homepage, the form sent data through POST action and the backend captured the data, processed it and added the data to the database. In the end, I was happy with the route below.
@app.route("/", methods=["GET", "POST"])
def index():
post_id = gen_id()
if request.method == "POST":
name = request.form.get("clip_name")
text = str(request.form.get("clip_text")).strip()
db.execute("INSERT INTO clips (clip_url, clip_name, clip_text, clip_time) VALUES (?, ?, ?, datetime('now', 'localtime'))", str(
post_id), name, text)
return redirect(f"/clip/{post_id}")
else:
return render_template("index.html")
As you can see, I kept it simple here. Before adding data, I am generating a unique post_id
or lets call that Clip_ID
which is nothing but the unique identifier for the clip. This is nothing complicated but just the hex of UUID4. I defined that in the additionals.py file which I thought is just a way of sorting the core routes and components from the additionals. Therefore, I created a file called additionals.py, which was looking something like this:
from uuid import uuid4
def gen_id():
return uuid4().hex
Simple! Right? YES, Damn Simple!
Also, I made sure that when the data was added, the page redirected to /clip/Clip_ID
where it displayed the added data. Coming to /clip/Clip_ID
, this was the second route I added.
The Second route is a dynamic route where the last parameter is the Clip_ID
which as I described earlier is nothing but a unique identifier for the clips. Here, I was taking that parameter and then sending that directly to the database query. If something returned, it processed that data and sent that to the clip.html page. Maintaining the simplicity, I wrote the following route.
@app.route("/clip/<clip_url_id>")
def clip(clip_url_id):
data = db.execute("SELECT clip_name, clip_text, clip_time FROM clips WHERE clip_url=?", clip_url_id)
if len(data) != 0:
text = str(data[0]["clip_text"])
name = data[0]["clip_name"]
time = data[0]["clip_time"]
return render_template("clip.html", url_id=clip_url_id, name=name, text=text, time=time)
else:
return render_template("error.html", code="That was not found on this server.")
As you can see, I also added a page called error.html which had a single h1 tag to display the error.
Now, I had a custom error page and a thought strikes my mind why not add custom error for basic errors codes such as 404, and 500. Like every good programmer, I was back at googling. After about 5-6 minutes, I came up copied the following code:
@app.errorhandler(404)
def error(code):
return render_template("error.html", code=code)
@app.errorhandler(500)
def error(code):
return render_template("error.html", code=code)
After adding this and the route to the about page, backend was done.
Frontend is something I do not know. Trust me, when I say I am not a good designer I am not kidding. Yes, I know, many experienced programmers must be thinking that this backend is trash, and now this person says that he doesn't even know frontend! Such a Shame. Please bear with me, trust me, the current version is much more optimized, secure and beautiful (kind of).
I know the title doesn't make any sense. Let me summarize the frontend development. I used HTML and Bootstrap to build frontend. For dynamic items such as page titles and stuff, I used Jinja (First time, huh? :) ). Jinja is essentially considered to be a part of backend but is written on the frontend. It is a web template engine, for Python Flask and Django. In general, Jinja is basically For-Loops and If-Else for HTML, but it is much more than that. Sample Jinja code looks something like this:
{% for i in range(1,11) %}
<h1>{{ i }}</h1>
{% endfor %}
Points to note about the sample code:
As I said earlier that, this was my CS50's Final project. I developed this whole in just one whole night and submitted at the very next morning.
By the way, you can download the project from final-project.zip and play with it in anyway you want.
Well, I submitted this in January, 2024, got my certificate and basically forgot about it until my python class in March, 2024.
In the March of 2024, we were asked to create a project that carried some weight and was good enough to be put on our resumes. At that time, ClipBin suddenly clicked on my mind and I thought, why not. I pitched the idea to our faculty, and he was good with it. So, I searched for my code archives and finally found the code. Till now, I never thought of making any changes to the orginal version as I thought that was good enough. And then I commit my biggest mistake (Not Actually) by showing this to my faculty, Dr. D. Saravanan Sir, on the very first review. He asked me for a live demo and then his initial response was "and now what?" and I was like, "Sir, this is it." and he seemed disappointed after hearing this. Then I said that I will be making some UI improvements to which he said "definitely this needs some improvements and features".
That day I learned a few lessons:
UI improvements sounds like a tedious job. Do you know, what's more tedious? Re-writing it from scratch. I believe that Bootstrap is not as good as Tailwind CSS, so I thought why not, let me, re-write the frontend in Tailwind. Now, next 2-3 hours, I spend rewriting the frontend, redesigning the homepage, clip page, about page and most the important, the error page.
Coming to the features, I thought why not add user login. I originally thought that when I was submitting this as Final Project but as I told you, I totally forgot about it. Well, I immediately started working on the backend, adding login and register routes but I absolutely had no idea how to implement that. I knew that Django had an authentication system built-in, but for now I was not thinking of migrating from Flask to Django. Again, like every good programmer, I spent next 15-20 minutes googling on how to implement that. However, I choose not to follow those and go back to what I submitted during my CS50.
CS50 had this problem statement called, CS50 Finance where we were to develop a Stock/Shares Management page which had the user login ability. So, I copied the backend for login and register from CS50 Finance, which was pretty solid. I added the frontend for both the routes, login.html and register.html which were pretty basic but working. I added the users table which stored the username and passwords of the users. I used Werkzeug Security for generating and checking password hashes when the user registered or tried logging in. This was fairly easy to implement, something just like this:
from werkzeug.security import generate_password_hash, check_password_hash
@app.route("/login")
def login():
...code...
if check_password_hash(pwd_hash, actual_pwd):
...code...
return True
@app.route("/register")
def register():
...code...
pwd_hash = generate_password_hash(actual_pwd)
...code...
With a login page, comes a dashboard page. ~ Me (Probably)
That said, my next target was to develop a dashboard page. Again, I tried keeping it simple by displaying the list of text and their ids which the user had saved. This had me thinking for a while that how do I do this without making changes to the current schema. Hence, I added another table where it stored all the relations between the user and the Clips they saved. I made sure, this only happens when they were logged in. Well, with this, I was able to fetch all the saved Clips on the dashboard page. I also added a Delete and View button to make the dashboard functional.
One feature I added was the ability to edit the text once it was saved. That only worked if the user who created, was logged in. No other user could save the changes. The users had a choice to make it editable while they were saving the text. To make it funny, I do not know why, but I made it like if the text was editable and if the users viewing it were logged in, it would show them that the text is editable and would allow them to make changes. But as soon as they pressed the Update button, I would simply return a "Not Enough Privillage" Error.
I almost forgot this. But when I remembered, I made/edited a basic route, i.e. /clip
to act like a searching route. What I did here was really simple, the search page had a simple input field and a search button. When the user entered the Clip_ID
, the backend simply ran a SELECT * FROM
SQL query, processed the result, and sent that to the frontend. On the frontend, all the data was shown in a Table-List like format.
Being a guy who is much interested in cybersecurity, I also added the ability to password protect the data. Although this was optional for the user, but this was essential for me. I added an input field on the homepage for the password. If the user entered a password, it would protect the text with a password. The users would be prompted to enter a password whenever they tried accessing a Password Protected Clip. This however did not encrypt the text at the database end. I or anyone who had access to the database would be able to view a Password Protected Text. I fixed this later with End-To-End Encryption.
Look I didn't want people to view what embarrassing I saved and also share the text to someone, both at the same time. This was the time I realized that I must add an option to unlist the Clips from search results and make them only accessable through their URL. So, I again added a checkbox on the homepage, programmed the backend, specially the search function to exclude the Clips that were Unlisted. This all took me a little longer than before to implement. But I enjoyed the process of bulding something that was coming out to be actually good.
At the end, this was the progress that I pitched at the second review. And yes, our faculty was happy with the progess I made. Although this was a group assignment, I was the only guy working on this.
During the second review, I got the idea why not add a feature where the user had an option to upload a file. This seemed like a good idea but ultimately I was the one who had to implement it. I decided to take a break here.
This was fairly easy to implement, I added a file input field on the homepage, which took the file and send to the backend. In the backend, I opened the file, read the content and stored the contents to the database. This however did take sometime to implement. Easy doesn't always mean faster development. I had to make sure there is enough input sanitization, you know, security mindset. With this, I completed my final review, second semister and passed my Python class. But there were many things planned for ClipBin by now. Also now I had a domain. Yes, this was the time, Apr 2024, when I finally bought a domain for the project. I wanted the domain to spell and read out ClipBin as whole, therefore I went with clipb.in which is clipbin if we remove that [dot].
I always wanted to develop an API, I was already 7 years into computer science but I never developed an API and this was my opportunity to build one. I started developing the API for ClipBin after taking a break of about 2-3 months. Initially the development was slow, but it paced up eventually. All I did was just remove the UI components from the base functions, and create a dedicated route for the API Calls.
With this, I launched the API in Oct 2024. This was big for me. I am still working to make the API more industry standard. One thing which was the hardest for me while developing the API was actually after the development, The Documentation. After several failed attempts of writing the documentaion myself, I asked one of my newest friend to write it for me, ChatGPT. Yes, I gave ChatGPT my whole code, and asked him to write the documentation for me. After a successful Prompt Engineering session with ChatGPT, I was happy with the documentation which GPT wrote. I added all the custom styling to the page and BAM! Production Friendly Documentation is now Ready!
Till now, I was using the CS50's Standard Python Library for connecting to database. However, this was the time I migrate to something better. I thought of migrating to PostgreSQL, but for now I would like to stick with SQLite. So, to remove the dependency on CS50's Standard Python Library and not rewrite the code according to sqlite module in Python. I decided to write a module which would act similar to CS50's Standard Python Library for connecting to database, executing queries, etc. Below is what I wrote:
import sqlite3
import threading
import logging
logging.basicConfig(level=logging.ERROR)
class SQLite:
def __init__(self, database_url):
self.database_url = database_url
self.lock = threading.Lock()
def _get_connection(self):
conn = sqlite3.connect(self.database_url, check_same_thread=False)
conn.row_factory = sqlite3.Row
return conn
def execute(self, query, *args):
try:
with self.lock:
conn = self._get_connection()
with conn:
cursor = conn.cursor()
cursor.execute(query, args)
if query.strip().upper().startswith(('INSERT', 'UPDATE', 'DELETE')):
conn.commit()
return True
result = cursor.fetchall()
return [dict(row) for row in result]
except sqlite3.Error as e:
logging.error("SQLite error: %s", e)
return None
finally:
if cursor:
cursor.close()
if conn:
conn.close()
def close(self):
pass
This was working fine for me so I decided to push this into production.
As I discussed earlier, when I was implementing password protection, I thought that one day I will implement End-To-End Encryption. For implementing E2EE, I used Python's Cryptography Module, which made it easy for me to implement the feature. I took help from ChatGPT for recommendations on the encryption. I originally designed something just like the diagram below:
This was easy to draw but a bit difficult to implement. And according to me, this was a pretty good design for E2EE. After some testing, and database redesign, i.e. making sure it is able to handle the encrypted data, this finally was push into Production on 6th Feb, 2025.
This is a project which I might never abandon, atleast until the project has reached its saturation point (i.e. no more development is possible). So that being said, the development continues. For now, I will probably focus more on fixing bugs rather than adding more features. Also, currently I am not accepting any contributions on GitHub. However, I definitely will accept contributions in the near future.
There comes the end. If you read till here, Thanks a lot for reading my very first blog/writeup. Feel free to give this a feedback by mailing me at aanis@clipb.in
Until Next time,
Cheers!