diceline-chartmagnifierquestion-marktwitter-whiteTwitter_Logo_Blue

Today I Learned

Use TouchID on MacBook Pro for Terminal sudo prompts

In order to use TouchID on MacBook Pro for Terminal sudo prompts, we need to enable Apple's Touch ID PAM module pam_tid.so (https://opensource.apple.com/source/pam_modules/pam_modules-173.1.1/modules/pam_tid/pam_tid.c.auto.html).

Just edit /etc/pam.d/sudo and add

auth sufficient pam_tid.so

Heads up, when you do a system update this change will be most probably overwritten. In order to make it persistent, you need to create a launchd daemon.

Create a new file called pam-tid.sh in a shared path

vim /Users/Shared/pam-tid.sh
#!/bin/bash

if ! grep 'pam_tid.so' /etc/pam.d/sudo --silent; then
  sed -i -e '1s;^;auth       sufficient     pam_tid.so\n;' /etc/pam.d/sudo
fi

Create a new com.graffino.pam.plist file:

vim /Users/Shared/com.graffino.pam.plist`

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.graffino.pam</string>

    <key>KeepAlive</key>
    <false/>

    <key>LaunchOnlyOnce</key>
    <true/>

    <key>RunAtLoad</key>
    <true/>

    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/pam-tid.sh</string>
    </array>
</dict>
</plist>

Start the daemon

Improving mapping performance with web workers

We can use web workers to execute the computationally intensive mapping operation in a separate thread. This can be done by following these steps:

  1. Create a new TS file for the worker
// worker.ts

// define the input and output types
type WorkerInput<T, U> = { data: T, mapFn: (x: T) => U };
type WorkerOutput<U> = { data: U[] };

// function for the actual work on the data
function doWork<T, U>(input: WorkerInput<T, U>): Promise<WorkerOutput<U>> {
  // Apply the mapping function to the input data.
  const mappedData = input.data.map(input.mapFn);

  // Return the mapped data as a promise.
  return Promise.resolve({ data: mappedData });
}
  1. Use the worker in the main.ts file
// main.ts
const worker = new Worker('worker.ts');

// Define the mapping function.
const mapFn = (x: number) => x * x;

// Send data and the mapping function to the worker for processing.
worker.postMessage({ data: [1, 2, 3], mapFn });

// Listen for the message with the result from the worker
worker.addEventListener('message', (event) => {
    console.log(`Received message from worker: ${event.data}`)
});

Matching dates within a monthly Interval that crosses over multiple years with PHP and Carbon

function getDatesInRange($dates, $start, $end) {
    $datesInRange = $dates->filter(function ($date) use ($start, $end) {
        if ((int) $start->format('Y') === (int) $end->format('Y')) {
            $start->year((int)$date->format('Y'));
            $end-> year((int)$date->format('Y'));
        } elseif ((int) $start->format('Y') < (int) $end->format('Y')) {
            if ((int) $date->format('m') >= (int) $start->format('m')) {
                $start->year((int) $date->format('Y'));
                $end->year((int) $date->format('Y'))->addYears(1);
            } else {
                $end->year((int) $date->format('Y'));
                $start->year((int) $date->format('Y'))->subYears(1);
            }
        }

        return Carbon::parse($date)->isBetween($start, $end);
    });

    return $datesInRange;
}

How to optimize the performance of the map function in JavaScript

To optimize the performance of a map function in JavaScript, you can do a few things:

  1. Use the forEach method instead of map if you are not creating a new array from the iteration. forEach is faster because it does not create a new array.

  2. Use the map method on a smaller array if possible. For example, if you have a large array and you only need to map over a subset of it, create a new array with just the subset and use map on that.

  3. If you are using map to transform each element of the array, use a specialized method for the transformation instead of using map. For example, if you are transforming each element to a string, use the toString method instead of using map.

  4. If you are using map to filter an array, use the filter method instead. filter is optimized for filtering and will be faster than using map for that purpose.

  5. Use a for loop instead of map if you need to perform complex operations on each element of the array. map is optimized for simple transformations, so a for loop will be faster for more complex operations.

Redirect unauthenticated users with meteor.js and react-router 6

The challenge I encountered was accessing the Meteor.user() in React Router's route loader property.

Alternatively, instead of querying the server, I'm picking up the user id from localStorage. The absence of it indicates the user is logged off or never logged in. Until Meteor kicks off, no sensitive data is displayed anyway.

...
{
  path: '/',
  element: <App />,
  errorElement: <ErrorPage />,
  loader: async () => window.localStorage.getItem('Meteor.userId'),
}
...

and in app.tsx:

const userId: UserId | null = useLoaderData()
const navigate = useNavigate()

useEffect(() => {
  if (userId === null) navigate('/login')
}, [userId])

Password-less SSH authentication on UniFi Dream Machine / Unifi Deam Machine Pro

Step 1. Make cron persist on restarts

unifi-os shell
curl -L https://github.com/unifi-utilities/unifios-utilities/raw/main/on-boot-script/packages/udm-boot_1.0.5_all.deb -o udm-boot.deb
dpkg -i udm-boot.deb
rm udm-boot.deb
exit

Step 2. Add root ssh keys on restart

cd /mnt/data/on_boot.d
vi 15-add-root-ssh-key.sh

File contents

#!/bin/sh

#####################################################
# ADD RSA KEYS AS BELOW - CHANGE BEFORE RUNNING     #
#####################################################
# set -- "ssh-rsa first key here all keys quoted" \ #
#        "ssh-rsa each line appended with slash " \ #
# 	 "ssh-rsa last one has no backslash"        #
#####################################################
set -- "ssh-rsa ..." \
        "ssh-rsa ...."

KEYS_FILE="/root/.ssh/authorized_keys"

counter=0
for key in "$@"
do
	# Places public key in ~/.ssh/authorized_keys if not present
	if ! grep -Fxq "$key" "$KEYS_FILE"; then
		let counter++
		echo "$key" >> "$KEYS_FILE"
	fi
done

echo $counter keys added to $KEYS_FILE

Make file executable and run it

chmod +x 15-add-root-ssh-key.sh 
./15-add-root-ssh-key.sh 

Step 3. Update banner

cat /dev/null > /issue
cat /dev/null > /etc/issue
cat /dev/null > /etc/motd

vi /etc/motd

# Insert your own banner 

Step 4. Update ssh configuration

UDM uses dropbear as ssh server and therefore the configuration is done on init.

Edit the dropbear configuration file

vi /etc/default/dropbear 
 
// See https://wiki.gentoo.org/wiki/Dropbear
DROPBEAR_OPTS="-sg"

Restart the dropbear service

/etc/init.d/dropbear restart

How to type-safely interact with Firestore documents

The problem

When using Typescript and Firestore, we usually have to do a lot of manual casting when working with documents. One such example would be getting the data of a document:

const thread = threadDocument.data(); // this will be of type any

Should we want to interact with the data in a type-safe manner, we'll have to cast it, which can quickly become tedious.

const thread = <ThreadData>threadDocument.data();

Additionally, when we write data to Firestore, there are no restrictions on how the data should look.

The solution

This is when Firestore Data Converters can come in handy. All we have to do is implement two methods - one where we constrain the data that gets written and one where we cast the data coming from Firestore:

const converter = {
  toFirestore: (dataToBeWritten: ThreadData) => data,
  fromFirestore: (document: QueryDocumentSnapshot) => <ThreadData>document.data(),
};

To take this one step further, we can store the "converted" collection reference so we won't have to apply the converters each time we query the collection:

const threadCollection = db.collection("threads").withConverter(converter);

Now we can safely interact with the collection without having to cast the data:

const threadDocument = await threadCollection.doc(id).get();
const thread = threadDocument.data(); // this will be of type ThreadData

How to obtain reactivity in custom hooks while interacting with the local storage

This is how we can obtain reactivity in our custom React.js hooks while working with the local storage, using the Pub/Sub (Observer) design pattern (with TypeScript support).

The goal is to implement a "useLocalStorage" custom hook, which will abstract away the complexity of reading from and writing to the local storage. As we know, each custom hook instantiates its own state. That is a problem in our case because when one instance of the hook updates the local storage, the state copies held by all the other hook instances will be out of sync and will never be updated.

We can solve this issue using the following idea: we can mimic a centralized shared state between our custom hook instances by delegating the responsibility of holding these in sync with the local storage to a custom "manager", the Observer object.

Our custom hook will work based on these ideas:

  1. Custom Hook instances will subscribe their inner state updater functions to this manager
  2. Custom Hook instances will publish the new state to the manager when updating a key of the local storage
  3. The manager will trigger all subscriber functions and thus update the inner states of the custom hook instances.

The observer object:

export type Listener<EventType> = (event: EventType) => void;

export type ObserverReturnType<KeyType, EventType> = {
  subscribe: (entryKey: KeyType, listener: Listener<EventType>) => () => void;
  publish: (entryKey: KeyType, event: EventType) => void;
};

export default function createObserver<
  KeyType extends string | number | symbol,
  EventType,
>(): ObserverReturnType<KeyType, EventType> {
  const listeners: Record<KeyType, Listener<EventType>[]> = {} as Record<
    KeyType,
    Listener<EventType>[]
  >;

  return {
    subscribe: (entryKey: KeyType, listener: Listener<EventType>) => {
      if (!listeners[entryKey]) listeners[entryKey] = [];
      listeners[entryKey].push(listener);
      return () => {
        listeners[entryKey].splice(listeners[entryKey].indexOf(listener), 1);
      };
    },
    publish: (entryKey: KeyType, event: EventType) => {
      if (!listeners[entryKey]) listeners[entryKey] = [];
      listeners[entryKey].forEach((listener: Listener<EventType>) =>
        listener(event),
      );
    },
  };
}

export const LocalStorageObserver = createObserver<
  LOCAL_STORAGE_KEYS,
  string
>();

export const { subscribe, publish } = LocalStorageObserver;

The useLocalStorage custom hook (window checks are optional, depending on which environment this JavaScript will run on):

export function useLocalStorage<T>(key: LOCAL_STORAGE_KEYS, initialValue: T) {
  const [storedValue, setStoredValue] = useState(() => {
    if (typeof window === 'undefined') {
      return initialValue;
    }
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  LocalStorageObserver.subscribe(key, setStoredValue);

  const setValue = (value: T) => {
    try {
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      LocalStorageObserver.publish(key, valueToStore);
      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      }
    } catch (error) {
      console.error(error);
    }
  };
  return [storedValue, setValue];
}