Chris Padilla/Blog
My passion project! Posts spanning music, art, software, books, and more. Equal parts journal, sketchbook, mixtape, dev diary, and commonplace book.
- Verifying it's an art post (based on which folder I select to save the image to)
- Creating a file with the post date in the filename (I usually put them together on Fridays, and share on Saturday)
- Writing details from the command line, such as title, alt text, and post body.
- Look up the user in the DB
- Verify the password
- Handle Matches
- Handle Mismatch
- Verify with the previous framework/library the encryption method
- If possible, transfer over the code/libraries used
- Wrap it in a
checkPassword()
function. - Designed to be secure by default and encourage best practices for safeguarding user data
- Uses Cross-Site Request Forgery Tokens on POST routes (sign in, sign out)
- Default cookie policy aims for the most restrictive policy appropriate for each cookie
- When JSON Web Tokens are enabled, they are encrypted by default (JWE) with A256GCM
- Auto-generates symmetric signing and encryption keys for developer convenience
- Optimize my images locally (something Cloudinary already automates, but I do by hand for...fun?!)
- Open up the Cloudinary portal
- Navigate to the right directory
- Upload the image
- Copy the url
- Paste the image into my markdown file
- Optionally add optimization tag if needed
Merry Christmas!
Silent Night
Merry Christmas from our familyβs oooooold piano!
Landscape Gestures
Generating Post Templates Programmatically with Python
I'm extending my quick script from a previous post on Automating Image Uploads to Cloudinary with Python. I figured β why stop there! Instead of just automating the upload flow and getting the url copied to my clipboard, I could have it generate the whole markdown file for me!
Form there, the steps that will be automated:
Here we go!
Reorganizing
First order of business is organizing the code. This was previously just a script, but now I'd like to encapsulate everything within a class.
In my index.py, I'll setup the class:
from dotenv import load_dotenv
load_dotenv()
import datetime
import cloudinary
import cloudinary.uploader
import cloudinary.api
import pyperclip
class ImageHandler():
"""
Upload Images to cloudinary. Generate Blog page if applicable
"""
def __init__(self, test: bool = False):
self.test = test
config = cloudinary.config(secure=True)
print("****1. Set up and configure the SDK:****\nCredentials: ", config.cloud_name, config.api_key, "\n")
Beneath, I'll wrap the previous code in a method called "upload_image()"
I'll still call the file directly to run the main script, so at the bottom of the file I'll add the code to do so:
if __name__ == '__main__':
ImageHandler().run()
So, on run, I want to handle the upload to Cloudinary. From the result, I want to get the image url back, and I want to know if I should start generating an art post:
def run(self):
image_url, is_art_image = self.upload_image()
print(is_art_image)
if is_art_image:
self.generate_blog_post(image_url=image_url)
To make that happen, I'm adding a bit of logic to image_upload
to check if I'm storing the image in my art folder:
def upload_image(self):
...
options = [
"/chrisdpadilla/blog/art",
"/chrisdpadilla/blog/images",
"/chrisdpadilla/albums",
]
is_art_image = selected_number == 0
...
selected_number = int(selected_number_input) - 1
is_art_image = selected_number == 0
...
return (res_url, is_art_image)
That tuple will then return string and boolean in a neat little package.
Now for the meat of it! I'll write out my generate_blog_post()
method.
Starting with date getting. Using datetime and timedelta, I'll be checking to see when the next Saturday will be (ART_TARGET_WEEKDAY is a constant set to 6 for Saturday):
def generate_blog_post(self, image_url: str = '') -> bool:
today = datetime.datetime.now()
weekday = today.weekday()
days_from_target = datetime.timedelta(days=ART_TARGET_WEEKDAY - weekday)
target_date = today + days_from_target
target_date_string = target_date.strftime('%Y-%m-%d')
Sometimes, I do this a week out, so I'll add some back and forth incase I want to manually set the date:
date_ok = input(f"{target_date_string} date ok? (Y/n): ")
if "n" in date_ok:
print("Set date:")
target_date_string = input()
print(f"New date: {target_date_string}")
input("Press enter to continue ")
From here, it's all command line inputs.
title = input("Title: ")
alt_text = input("Alt text: ")
caption = input("Caption: ")
And then I'll use a with statement to do my file writing. The benefit of using the with
keyword here is that it will handle file closing automatically.
md_body = SKETCHES_POST_TEMPLATE.format(title, target_date_string, alt_text, image_url, caption)
with open(BLOG_POST_DIR + f"/sketches-{target_date_string}.md", "w") as f:
f.write(md_body)
return True
ViolΓ ! Running through the command line now generates a little post! Lots of clicking and copy-and-pasting time saved! π
Angels We Have Heard on High
Taeko Onuki
New Album β Ice π§
Music somewhere between ambient classical and winter lofi!
Purchase on π€ Bandcamp and Listen on π Spotify or any of your favorite streaming services!
Credentials Authentication in Next.js
Taking an opinionated approach, Next Auth intentionally limits the functionality available for using credentials such as email/password for logging in. The main limit is that this forces a JWT strategy instead of using a database session strategy.
Understandably so! The number of data leaks making headlines, exposing passwords, has been a major security issue across platforms and services.
However, the limitation takes some navigating when you are migrating from a separate backend with an existing DB and need to support older users that created accounts with the email/password method.
Here's how I've been navigating it:
Setup Credentials Provider
Following the official docs will get you most of the way there. Here's the setup for my authOptions in app/api/auth/[...nextAuth]/route.js
:
import CredentialsProvider from "next-auth/providers/credentials";
...
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
username: {label: 'Username', type: 'text', placeholder: 'your-email'},
password: {label: 'Password', type: 'password', placeholder: 'your-password'},
},
async authorize(credentials, req) {
...
},
}),
Write Authorization Flow
From here, we need to setup our authorization logic. We'll:
async authorize(credentials, req) {
try {
// Add logic here to look up the user from the credentials supplied
const foundUser = await db.collection('users').findOne({'unique.path': credentials.email});
if(!foundUser) {
// If you return null then an error will be displayed advising the user to check their details.
return null;
// You can also Reject this callback with an Error thus the user will be sent to the error page with the error message as a query parameter
}
if(!foundUser.unique.path) {
console.error('No password stored on user account.');
return null;
}
const match = checkPassword(foundUser, credentials.password);
if(match) {
// Important to exclude password from return result
delete foundUser.services;
return foundUser;
}
} catch (e) {
console.error(e);
}
return null;
},
PII
The comments explain away most of what's going on. I'll explicitly note that here I'm using a try/catch block to handle everything. When an error occurs, the default behavior is for the error to be sent to the client and displayed. Even an incorrect password error could cause a Personally Identifiable Information (PII) error. By catching the error, we could log it with our own service and simple return null for a more generic error of "Failed login."
Custom DB Lookup
I'll leave explicit details out from here on how a password is verified for my use case. But, a general way you may approach this when migration:
Sending Passwords over the Wire?
A concern that came up for me: We hash passwords to the database, but is there an encryption step needed for sending it over the wire?
Short answer: No. HTTPS covers it for the most part.
Additionally, Next auth already takes many security steps out of the box. On their site, they list the following:
CSRF is the main concern here, and they have us covered!
Integrating With Other Providers
Next Auth also allows for using OAuth sign in as well as tokens emailed to the clients. However, it's not a straight shot. Next requires a JWT strategy, while emailing tokens requires a database strategy.
There's some glue that needs adding from here. A post for another day!
Fantasia!
Automating Image Uploads to Cloudinary with Python
There's nothing quite like the joy of automating something that you do over and over again.
This week I wrote a python script to make my life easier with image uploads for this blog. The old routine:
I can eliminate most of those steps with a handy script. Here's what I whipped up, with some boilerplate provided by the Cloudinary SDK quick start guide:
from dotenv import load_dotenv
load_dotenv()
import cloudinary
import cloudinary.uploader
import cloudinary.api
import pyperclip
config = cloudinary.config(secure=True)
print("****1. Set up and configure the SDK:****\nCredentials: ", config.cloud_name, config.api_key, "\n")
print("Image to upload:")
input1 = input()
input1 = input1.replace("'", "").strip()
print("Where is this going? (Art default)")
options = [
"/chrisdpadilla/blog/art",
"/chrisdpadilla/blog/images",
"/chrisdpadilla/albums",
]
folder = options[0]
for i, option in enumerate(options):
print(f'{i+1} {option}')
selected_number_input = input()
if not selected_number_input:
selected_number_input = 1
selected_number = int(selected_number_input) - 1
if selected_number <= len(options):
folder = options[selected_number]
res = cloudinary.uploader.upload(input1, unique_filename = False, overwrite=True, folder=folder)
if res.get('url', ''):
pyperclip.copy(res['url'])
print('Uploaded! Url Coppied to clipboard:')
print(res['url'])
Now, when I run this script in the command line, I can drag an image in, the script will ask where to save the file, and then automatically copy the url to my clipboard. Magic! β¨
A couple of steps broken down:
Folders
I keep different folders for organization. Album art is in one. Blog Images in another. Art in yet another. So first, I select which one I'm looking for:
print("Where is this going? (Art default)")
options = [
"/chrisdpadilla/blog/art",
"/chrisdpadilla/blog/images",
"/chrisdpadilla/albums",
]
folder = options[0]
for i, option in enumerate(options):
print(f'{i+1} {option}')
selected_number_input = input()
and later on, that's passed to the cloudinary API as a folder:
if not selected_number_input:
selected_number_input = 1
selected_number = int(selected_number_input) - 1
if selected_number <= len(options):
folder = options[selected_number]
res = cloudinary.uploader.upload(input1, unique_filename = False, overwrite=True, folder=folder)
Copying to clipboard
Definitely the handiest, and it's just a quick install to get it. I'm using pyperclip to make it happen with this one liner:
if res.get('url', ''):
pyperclip.copy(res['url'])
Clementi - Sonatina in F Maj Exposition
Note to self: don't wait until a couple of weeks after practicing something to record π
Blue Hair, Don't Care
Next Auth Custom Session Data
I've been tinkering with Next Auth lately, getting familiar with the new App Router and React Server Components. Both have made for a big paradigm shift, and a really exciting one at that!
With all the brand new tech, and with many people hard at work on Next Auth to integrate with all of the new hotness, there's still a bit of transition going on. For me, I found I had to do a bit more digging to really setup Next Auth in my project, so here are some of the holes that ended up getting filled:
Getting User Data from DB through JWT Strategy
When you use a database adapter, Next auth automates saving and update user data. When migrating an existing app and db to Next auth, you'll likely want to handle the db interactions yourself to fit your current implementation.
Here's what the authOptions looked like for an OAuth provider:
export const authOptions = {
// adapter: MongoDBAdapter(db),
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
session: {
jwt: true,
maxAge: 30 * 24 * 60 * 60,
},
}),
],
secret: process.env.NEXTAUTH_SECRET,
};
Notice that I'm leaving the adapter above out and using the jwt strategy here.
There's a bit of extra work to be done here. The session will save the OAuth data and send it along with the token. But, more than likely, you'll have your own information about the user that you'd like to send, such as roles within your own application.
To do that, we need to add a callbacks object to the authOptions with a jwt
and session
methods:
async jwt({token, user}) {
if(user) {
token.user = user;
const {roles} = await db.users.findOne(query)
token.roles = roles;
}
return token;
},
async session({session, token}) {
if(token.roles) {
session.roles = token.roles;
}
return session;
},
So there's a bit of hot-potato going on. On initial sign in, we'll get the OAuth user data, and then reference our db to find the appropriate user. From there, we pass that to the token, which is then extracted into the session later on.
Once that's set, you'll want to pass these authOptions
in every time you call getServerSession
so that these callbacks are used to grab the dbUser
field. Here's an example in a server action:
import React from 'react';
import {getServerSession} from 'next-auth';
import { authOptions } from '@api/[...nextauth]/route';
import Button from './Button';
export default async function ServerActionPage() {
const printName = async () => {
'use server';
const session = await getServerSession(authOptions);
console.log(session);
return session?.user?.name || 'Not Logged In';
};
return (
<div className="m-20">
<Button action={printName} />
</div>
);
}
When that's logged, we'll get the OAuth user info and the roles we passed in from our db:
{
user: {...}
roles: [...]
}
Just Friends
Lovers no more~