Develumpen

Edit a specific git commit

I was on a trip with my work laptop when I decided to start a new personal Rails engine. I didn't realize that Rails would take my current git config info to fill in the .gemspec file. By the time I updated the git config for the project, my work email was already there.

With a mix of Stack Overflow and Copilot, here's how I fixed it:

$ git rebase -i HEAD~n
# In the editor, change 'pick' to 'edit' for the commit you want to fix
$ git add <file>
$ git commit --amend
$ git rebase --continue

I wanted to edit the first commit of the history and running the first command showed an error about an invalid upstream. This is where Copilot helped, even though I later realized somebody else had commented about this topic on Slack Overflow as well.

To edit the first commit, run the following command:

$ git rebase -i --root

I had to push force, but I didn't mind for this project.
#git

See if a React property has changed

Sometimes you want to avoid unnecessary renders without knowing what you're doing. useCallback here, useMemo there, but nothing seems to work.

With the help of useRef you can easily track what properties are changing thus triggering a useEffect hook. Replace callback with the name of the property you want to track.

const prevCallbackRef = useRef(callback);

useEffect(() => {
  if (prevCallbackRef.current !== callback) {
    console.log('callback has changed');
  }

  // Update refs with current values
  prevCallbackRef.current = callback;
}, [callback]);
#react

indieblog.xyz can now syndicate content to X 🎉

#indieblog

Ruby on Rails, SSL in localhost, Omniauth & Sign in with Apple

Even if it's pretty simple to roll out your own authentication system with Rails, I've decided (again) to use OmniAuth. This time I'm using it with Apple, as I'm thinking on creating a Turbo Native app for iOS as well.

OmniAuth is a simple gem to install and use. It makes OAuth integration with your app a breathe. But once OmniAuth is installed and configured you have to choose which strategies install and, as always, Apple doesn't make it easy.

I'm going to document the process because I might follow the same path in the future.

First thing is to install OmniAuth and the Apple strategy. I'm not going to explain this step because the official docs are really good.

In order to test Sign in with Apple you're going to need an SSL certificate. There are many ways to do this. Because Apple won't let me use localhost as a domain, I'm going to use lvh.me, that is a domain that points to 127.0.0.1

I found this guide on how to test SSL in localhost. All the credits to Decidim. I'm just copy/pasting and adapting their code to fit my needs.

Run this command to generate a key and certificate. I copied my files to /config/ssl.
openssl req -x509 -out lvhme.crt -keyout lvhme.key \
  -newkey rsa:2048 -nodes -sha256 \
  -subj '/CN=lvh.me' -extensions EXT -config <( \
   printf "[dn]\nCN=lvh.me\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:lvh.me\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")

Then, because I use bin/dev, I updated the Procfile to run rails with the certificate:
web: bin/rails server -b "ssl://lvh.me:3443?key=config/ssl/lvhme.key&cert=config/ssl/lvhme.crt"

Edit /config/development.rb and add:
config.force_ssl = true
config.hosts << "lvh.me"

Start the server and the web will be accessible on https://lvh.me:3443. You'll see a warning because the certificate is self-signed, but it's ok. This is enough for Sign in with Apple to work.

Everything should work by now. But it doesn't. If you try to sign in, you'll be redirected to an Apple website, but when Apple redirects back to your site, Rails will complain.

First thing is about the redirect method. This route should work fine:
match "/auth/:provider/callback", to: "sessions#create", via: [ :get, :post ]

The second issue is related to CSRF. Usually disabling authenticity token verification in your controller/action is enough:
skip_forgery_protection only: [ :create ]

But with Apple this is not enough. It took me some internet digging to find a solution. This Github comment from last year fixed it:

In the OmniAuth callback controller,
private
  
def verified_request?
  action_name == 'apple' || super
end

And in /config/application.rb
config.action_dispatch.cookies_same_site_protection = lambda { |request|
  request.path == '/users/auth/apple' ? :none : :lax
}

If everything went as expected, when Apple redirects to your Rails app, the signed user's info should be accessible from request.env["omniauth.auth"].

Keep in mind that Apple will only send user's name and email the first time the user signs in. After the first sign in, you'll only receive tokens and identifiers.

This is the content of request.env["omniauth.auth"] including name and email:

{
  "provider": "apple",
  "uid": "",
  "info": {
    "sub": "",
    "email": "",
    "first_name": "",
    "last_name": "",
    "name": "",
    "email_verified": true,
    "is_private_email": false
  },
  "credentials": {
    "token": "",
    "refresh_token": "",
    "expires_at": 1725018440,
    "expires": true
  },
  "extra": {
    "raw_info": {
      "id_info": {
        "iss": "https://appleid.apple.com",
        "aud": "",
        "exp": 1725101240,
        "iat": 1725014840,
        "sub": "",
        "nonce": "",
        "at_hash": "",
        "email": "",
        "email_verified": true,
        "auth_time": 1725014838,
        "nonce_supported": true
      },
      "user_info": {
        "name": {
          "firstName": "",
          "lastName": ""
        },
        "email": ""
      },
      "id_token": ""
    }
  }
}
#ruby on rails #ssl #omniauth #apple

Generated columns, SQLite & Ruby on Rails

I keep creating small useless apps. One of the apps I'm currently working on is a small personal CRM or PRM.

The app has a person table with first_name and last_name columns. I want to be able to search a person by first and last name, and after doing some research I learned about generated columns.
This time I decided to stick with SQLite and SQLite supports generated columns.

Long story short, a generated column is a column that you have access to, but you cannot update directly. The content is generated by SQLite based on an expression you provide on table creation. This expression can reference other columns.

In this case I'm using the generated column to store a concatenation of strings on a column named full_name.

The table looks something like this (simplified):

CREATE TABLE person(
  first_name TEXT,
  last_name TEXT,
  full_name TEXT GENERATED ALWAYS AS (concat(first_name, ' ', last_name)) STORED
);
You can omit the GENERATED ALWAYS, and you can choose between STORED or VIRTUAL. You can imagine what these keyword do.

Instead of concat you can do almost whatever you want in there, even math calculations. There is a list of limitations that you better have in mind before creating your table, like you cannot add a stored column after the table has been created (but you can add virtual columns).

I believe another way to concatenate strings is with the || operator but I couldn't find info on how to use it (ok, I didn't spend much time looking for it because concat worked just fine).

Before knowing about generated columns I used to do this on a Rails model callback.

There is some info about this in the official Rails docs. The docs are for PostgreSQL, but support for SQLite was merged to master in late 2023.

A Rails migration for creating the same person table would look like:

create_table :people do |t|
  t.string :first_name
  t.string :last_name
  t.virtual :full_name, type: :string, as: "concat(first_name, ' ', last_name)", stored: true
end
#sqlite #ruby on rails