<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: H. Kamran</title>
    <description>The latest articles on DEV Community by H. Kamran (@hkamran).</description>
    <link>https://dev.to/hkamran</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F240388%2F781d602b-d0f4-43fc-9458-5ccfbcf7a221.jpg</url>
      <title>DEV Community: H. Kamran</title>
      <link>https://dev.to/hkamran</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hkamran"/>
    <language>en</language>
    <item>
      <title>Remotely Access Your Home Assistant Instance Securely</title>
      <dc:creator>H. Kamran</dc:creator>
      <pubDate>Sat, 04 May 2024 00:31:57 +0000</pubDate>
      <link>https://dev.to/hkamran/remotely-access-your-home-assistant-instance-securely-38la</link>
      <guid>https://dev.to/hkamran/remotely-access-your-home-assistant-instance-securely-38la</guid>
      <description>&lt;p&gt;If you want to access your Home Assistant instance outside your local network, you have a few options. You could try port-forwarding port 8123 or whatever port you use, expose your reverse proxy, or you could sign up for Nabu Casa's subscription service. But what about Cloudflare Tunnels?&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we begin, there are two things you'll need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Cloudflare account&lt;/li&gt;
&lt;li&gt;A domain linked to that Cloudflare account&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Set up the tunnel
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Create the tunnel
&lt;/h3&gt;

&lt;p&gt;First, connect the tunnel from your server to Cloudflare. If you have not already done so, create a new tunnel in the &lt;a href="https://one.dash.cloudflare.com"&gt;Zero Trust dashboard&lt;/a&gt; by going to Networks &amp;gt; Tunnels. Give your tunnel a name that means something to you (only you will be able to see it in this dashboard). Select the Docker tab and copy the &lt;code&gt;docker run&lt;/code&gt; command because we will need it in a second. Next, let's configure traffic routing from the tunnel to the service. Enter the domain and subdomain, then select the type of service. Since this is for Home Assistant, set the type to &lt;code&gt;HTTP&lt;/code&gt;. Assuming that you followed &lt;a href="https://www.home-assistant.io/installation/alternative/#docker-compose"&gt;the instructions on setting Home Assistant up with Compose&lt;/a&gt;, your Home Assistant container's &lt;code&gt;network_mode&lt;/code&gt; is &lt;code&gt;host&lt;/code&gt;, therefore, the service URL should be &lt;code&gt;host.docker.internal:8123&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect the tunnel
&lt;/h3&gt;

&lt;p&gt;Because this guide assumes you are using Docker Compose, add a new service for the tunnel connector in your &lt;code&gt;compose.yml&lt;/code&gt; or &lt;code&gt;docker-compose.yml&lt;/code&gt; file that you are using for Home Assistant. Make sure to replace &lt;code&gt;[your token here]&lt;/code&gt; with the token from the &lt;code&gt;docker run&lt;/code&gt; command you copied earlier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tunnel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudflare/cloudflared:latest&lt;/span&gt;
  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tunnel --no-autoupdate run --token [your token here]&lt;/span&gt;
  &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;homeassistant&lt;/span&gt; &lt;span class="c1"&gt;# or whatever the Home Assistant service (not the container name!) is named in the Compose file&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;TZ&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[your&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;timezone]"&lt;/span&gt;
  &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
  &lt;span class="na"&gt;extra_hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host.docker.internal:host-gateway"&lt;/span&gt; &lt;span class="c1"&gt;# Required to use `host.docker.internal`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the Compose file has been updated, run &lt;code&gt;docker compose up -d&lt;/code&gt; or the equivalent in whatever container system is running on your system. This will pull the &lt;code&gt;cloudflared&lt;/code&gt;&lt;sup id="fnref1"&gt;1&lt;/sup&gt; image and then establish a tunnel to Cloudflare. If all is well, you should be able to reload the Zero Trust dashboard and see that the tunnel status is healthy. Try visiting your site in a new tab and see if it is accessible. If you get a 400 Bad Request error, you may need to update the &lt;a href="https://www.home-assistant.io/integrations/http/#reverse-proxies"&gt;&lt;code&gt;http.trusted_proxies&lt;/code&gt; array&lt;/a&gt; in your &lt;code&gt;configuration.yaml&lt;/code&gt; file. For more information on &lt;code&gt;host.docker.internal&lt;/code&gt;, check out &lt;a href="https://stackoverflow.com/a/72828318"&gt;this Stack Overflow answer&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secure the tunnel
&lt;/h2&gt;

&lt;p&gt;At this point, your tunnel is set up and you can use Home Assistant from outside your network, but right now it is very insecure.&lt;/p&gt;

&lt;p&gt;There are two parts to this step. Part one allows connections from any browser or device, as long as it is not the mobile apps. Part two allows connections exclusively from the mobile apps. For the best experience, follow both parts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Part One: Cloudflare Access
&lt;/h3&gt;

&lt;p&gt;Cloudflare Access is part of Cloudflare's Zero Trust offering. It is designed to secure apps by placing an identity portal in front. To configure it, go to the Zero Trust dashboard, then Access &amp;gt; Applications. Ensure you have configured an identity provider in Settings &amp;gt; Authentication first. Click "Add an application", then select the "Self-hosted" option. Choose an application name, and be aware that this will be visible to those who visit the public URL. I set mine to "Home Assistant". After that, enter the same domain and subdomain that you configured for the tunnel. Then click "Next" to advance to the policy configuration page.&lt;/p&gt;

&lt;p&gt;The policy configuration page is where you configure who can access the site. If you have previously created &lt;a href="https://developers.cloudflare.com/cloudflare-one/identity/users/groups/"&gt;Access Groups&lt;/a&gt;, you can select those here. Regardless of whether you use a group or not, you still have to enter a policy name and add your rules. For example, I configured my policies to only allow users who have specific emails and are in the United States. I did this by going to the "Include" section, setting the selector to "Emails", then adding comma-separated emails in the value field. I then added a "Require" rule that ensures the visitor is in the U.S.&lt;/p&gt;

&lt;p&gt;Finally, click "Next" and then "Add application". Your Home Assistant instance is now secured. Try visiting your site again from a normal browser window and also from a private window.&lt;/p&gt;

&lt;p&gt;The downside to using Access is that the Home Assistant mobile apps cannot handle it right now,&lt;sup id="fnref2"&gt;2&lt;/sup&gt; but there is another way to secure the tunnel and allow the mobile apps to work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Part Two: mTLS Certificates
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.cloudflare.com/learning/access-management/what-is-mutual-tls/"&gt;mTLS certificates&lt;/a&gt;, or mutual TLS certificates, are a way of securely connecting to services without credentials. Cloudflare has support for this through the Zero Trust dashboard, but that requires an Enterprise plan. Instead, we will use their client certificates offering.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The iOS companion app does not support mTLS. See the discussion in &lt;a href="https://github.com/home-assistant/iOS/discussions/1788"&gt;#1788&lt;/a&gt; and the comment from Franck Nijhof (one of the Home Assistant maintainers) on &lt;a href="https://github.com/home-assistant/iOS/pull/2144#issuecomment-1992395096"&gt;PR #2144&lt;/a&gt; for more information.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To get started, one change needs to be made to the tunnel configuration. Go to Access &amp;gt; Tunnels, and select the tunnel you created earlier and click the "Configure" button. Under the "Public Hostname" tab, add another hostname. The service options should be set to the same thing you have set for the existing hostname. I recommend using the same domain and subdomain as the main hostname with &lt;code&gt;-mobile&lt;/code&gt; added to the subdomain. For example, if the main subdomain is &lt;code&gt;ha&lt;/code&gt;, then use &lt;code&gt;ha-mobile&lt;/code&gt; as the subdomain for this hostname.&lt;/p&gt;

&lt;p&gt;Next, go to the main Cloudflare dashboard and open the &lt;a href="https://dash.cloudflare.com/?to=/:account/:zone/ssl-tls/client-certificates"&gt;Client Certificates page&lt;/a&gt; (under SSL/TLS). Click "Create Certificate" and ensure the private key type is set to "RSA (2048)". I left the certificate validity at 10 years. Then create the certificate.&lt;/p&gt;

&lt;p&gt;Ensure the key format is &lt;code&gt;PEM&lt;/code&gt;, then copy the certificate into a file called &lt;code&gt;cf-client.pem&lt;/code&gt;, and the private key into &lt;code&gt;cf-client.key&lt;/code&gt;. You can change these filenames, but I recommend keeping the extensions. &lt;strong&gt;Make sure you make a backup&lt;/strong&gt; as you will not be able to see this certificate again. Under the "Hosts" header, click the "Edit" button next to "None", then type in the second URL you created for the tunnel, e.g. &lt;code&gt;ha-mobile.yourdomain.com&lt;/code&gt;. Click "Save". This ensures that the subdomain can be used with mTLS.&lt;/p&gt;

&lt;p&gt;Now that the certificate has been created, it needs to be transferred to a device running the mobile app. But before that, it needs to be converted into a &lt;a href="https://en.wikipedia.org/wiki/PKCS_12"&gt;PKCS #12&lt;/a&gt; (&lt;code&gt;PFX&lt;/code&gt;) archive. Assuming your computer has OpenSSL installed, it's easy to generate a &lt;code&gt;PFX&lt;/code&gt; file. In the same directory as the &lt;code&gt;.pem&lt;/code&gt; and &lt;code&gt;.key&lt;/code&gt; files you created earlier, run the following command, replacing &lt;code&gt;cf-client.key&lt;/code&gt; and &lt;code&gt;cf-client.pem&lt;/code&gt; with your filenames:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl pkcs12 &lt;span class="nt"&gt;-export&lt;/span&gt; &lt;span class="nt"&gt;-out&lt;/span&gt; cf-client.pfx &lt;span class="nt"&gt;-inkey&lt;/span&gt; cf-client.key &lt;span class="nt"&gt;-in&lt;/span&gt; cf-client.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A prompt for an export password will appear. Android failed to install the &lt;code&gt;PFX&lt;/code&gt; I generated without a password, so make sure to add one. After that, you should have a &lt;code&gt;PFX&lt;/code&gt; file that is ready to be installed. Transfer it securely to your device and install it by tapping the file. Enter the export password you created, then select "VPN &amp;amp; app user certificate" in the pop-up. Your certificate is now installed and ready for use.&lt;/p&gt;

&lt;p&gt;Open the Home Assistant app, select your server, and set your external URL (the "Home Assistant URL") to the URL you set up for mTLS authentication. If you are connected to your local network, disconnect and try accessing Home Assistant. You should be able to use Home Assistant outside your local network now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;If you have any questions, need any help, or have any suggestions, feel free to contact me on &lt;a href="https://twitter.com/hkamran80"&gt;Twitter&lt;/a&gt;, &lt;a href="https://vmst.io/@hkamran"&gt;Mastodon&lt;/a&gt;, or &lt;a href="https://bsky.app/profile/hkamran.com"&gt;Bluesky&lt;/a&gt;, or leave a comment.&lt;/p&gt;

&lt;p&gt;If you have any improvements to any of my articles or notes, please leave a comment or &lt;a href="https://github.com/hkamran80/articles#contributions"&gt;submit a pull request&lt;/a&gt;.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;The name &lt;code&gt;cloudflared&lt;/code&gt; comes from the Unix tradition of naming servers with a "-d" suffix standing for "daemon". (text adapted from &lt;a href="https://blog.cloudflare.com/workerd-open-source-workers-runtime"&gt;Cloudflare&lt;/a&gt;) ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;The Home Assistant maintainers have rejected all attempts to add additional authentication methods to the companion app, as evidenced by &lt;a href="https://github.com/home-assistant/android/pull/3510#issuecomment-1927928037"&gt;PR #3510&lt;/a&gt;, &lt;a href="https://github.com/home-assistant/android/pull/4160#issuecomment-1927929682"&gt;PR #4160&lt;/a&gt;, &lt;a href="https://github.com/home-assistant/iOS/pull/2144#issuecomment-1992395096"&gt;PR #2144&lt;/a&gt; and &lt;a href="https://github.com/home-assistant/android/issues/167#issuecomment-566918860"&gt;issue #167&lt;/a&gt;. The official guidance from them is to use a browser instead. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>security</category>
      <category>guide</category>
      <category>homeassistant</category>
      <category>cloudflaretunnels</category>
    </item>
    <item>
      <title>Securing Your Digital Life</title>
      <dc:creator>H. Kamran</dc:creator>
      <pubDate>Fri, 16 Jun 2023 06:46:42 +0000</pubDate>
      <link>https://dev.to/hkamran/securing-your-digital-life-49kn</link>
      <guid>https://dev.to/hkamran/securing-your-digital-life-49kn</guid>
      <description>&lt;p&gt;Your digital self controls your life. Lose your password, and you're locked out of a potentially important email or bank account. It's even worse if you don't have any recovery systems set up. But what's worse is if you are hacked. Your digital self, ripped away. Your social media accounts, your connection to friends, family, and others, disappearing in an instant. However, there are proactive measures you can take.&lt;/p&gt;

&lt;h2&gt;
  
  
  Passwords
&lt;/h2&gt;

&lt;p&gt;The first thing you can do is ensure you have strong passwords. Try entering your most commonly used passwords into &lt;a href="https://www.security.org/how-secure-is-my-password/"&gt;Security.org's tool&lt;/a&gt;, and see how long it takes to crack your password. According to a 2021 Statista survey, 64% of U.S. respondents had passwords 8–11 characters.&lt;sup id="fnref1"&gt;1&lt;/sup&gt; Assuming a mix of numbers, uppercase and lowercase letters, an 11-character password will take ten months to crack.&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The easiest way to secure your password is to get a password manager. I recommend against using Google's password manager, given that it's built into your browser, so if your browser is compromised, there goes your passwords. If you use an iOS/iPadOS or Safari on macOS, Apple has a password manager built-in that's secured by end-to-end encryption, as well as encrypted locally by the onboard chips until the password is needed. Alternatively, if you use multiple platforms, or prefer not to use Apple's offering, there's &lt;a href="https://bitwarden.com/"&gt;Bitwarden&lt;/a&gt;, &lt;a href="https://1password.com/"&gt;1Password&lt;/a&gt;, and &lt;a href="https://www.dashlane.com/"&gt;Dashlane&lt;/a&gt; to name a few. Definitely don't use LastPass, their security has been well covered &lt;a href="https://www.theverge.com/2023/2/28/23618353/lastpass-security-breach-disclosure-password-vault-encryption-update"&gt;in&lt;/a&gt; &lt;a href="https://www.forbes.com/sites/daveywinder/2023/03/03/why-you-should-stop-using-lastpass-after-new-hack-method-update/"&gt;the&lt;/a&gt; &lt;a href="https://arstechnica.com/information-technology/2023/02/lastpass-hackers-infected-employees-home-computer-and-stole-corporate-vault/"&gt;media&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you choose to use a third-party password manager, make sure to set a strong password for that. Bitwarden created a &lt;a href="https://bitwarden.com/password-generator/"&gt;password and passphrase generator&lt;/a&gt; that you can use to create a master password.&lt;/p&gt;

&lt;p&gt;No matter what password manager you use, the first step is to add all your existing passwords to it. Whenever you create a new account, generate a strong password using the tool. I recommend at least 20 characters using a mix of numbers, uppercase and lowercase letters, and four or more symbols. Go through all your passwords and strengthen them by changing your passwords.&lt;/p&gt;

&lt;h3&gt;
  
  
  Breach Alerts
&lt;/h3&gt;

&lt;p&gt;The number one rule when it comes to passwords is &lt;strong&gt;do not reuse them&lt;/strong&gt;. Many services, including 500px, Adobe, Audi, Bitly, and &lt;a href="https://haveibeenpwned.com/PwnedWebsites"&gt;many others&lt;/a&gt;, have been breached, resulting in passwords, emails, and other sensitive information being stolen. A wonderful, free service called &lt;a href="https://haveibeenpwned.com/"&gt;Have I Been Pwned&lt;/a&gt; (HIBP), created by security researcher &lt;a href="https://www.troyhunt.com/about/"&gt;Troy Hunt&lt;/a&gt; and used by companies and &lt;a href="https://www.troyhunt.com/tag/government/"&gt;governments&lt;/a&gt; around the world, shows users which of their accounts have been breached&lt;sup id="fnref3"&gt;3&lt;/sup&gt;, and can also inform them as soon as a breach is published. I highly recommend putting your emails into the service to check if any associated accounts have been breached, and if they have, change the passwords. The other thing I highly recommend is &lt;a href="https://haveibeenpwned.com/NotifyMe"&gt;signing up for HIBP notifications&lt;/a&gt; on all your emails. This will allow you to know almost immediately if your account was leaked in a breach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-factor Authentication
&lt;/h2&gt;

&lt;p&gt;Multi-factor authentication, also known as two-factor authentication, 2FA, or MFA, is where a user is required to enter two or more verification methods before authentication can proceed. You should enable multi-factor authentication on all your accounts, especially for those accounts that secure personal identifiable information (PII)&lt;sup id="fnref4"&gt;4&lt;/sup&gt;. Some key accounts that should absolutely be secured, even if you don't use MFA on any other service, include your financial accounts (including revenue agencies like the U.S. Internal Revenue Service, the Canada Revenue Agency, AFIP, etc.), your government accounts (e.g. Login.gov, Singpass, etc.), and your healthcare accounts (e.g. insurance, patient portals (like MyChart), etc.).&lt;/p&gt;

&lt;p&gt;Don't just take my word for it. Many government agencies, including, but certainly not limited to, the &lt;a href="https://www.cisa.gov/MFA"&gt;U.S. Cybersecurity and Infrastructure Security Agency (CISA)&lt;/a&gt;, the &lt;a href="https://www.ncsc.gov.uk/guidance/setting-2-step-verification-2sv"&gt;UK National Cyber Security Centre&lt;/a&gt;, and the &lt;a href="https://www.cyber.gov.au/learn-basics/explore-basics/mfa"&gt;Australian Cyber Security Centre&lt;/a&gt;, recommend MFA.&lt;/p&gt;

&lt;p&gt;When you enable MFA, try to avoid text messages, phone calls, and email as authentication methods. Text messages and phone calls are susceptible to a SIM swap scam, which is when a malicious person contacts a mobile service provider to change the SIM which has phone calls and text messages routed to it. For more information on SIM swap scams, check out &lt;a href="https://wikipedia.org/wiki/SIM_swap_scam"&gt;the Wikipedia page&lt;/a&gt; or &lt;a href="https://us.norton.com/blog/mobile/sim-swap-fraud"&gt;this Norton article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The best method to use is &lt;a href="https://fidoalliance.org/fido2/"&gt;FIDO2&lt;/a&gt;, also known as &lt;a href="https://www.yubico.com/authentication-standards/webauthn/"&gt;WebAuthn&lt;/a&gt;. FIDO2 uses physical security keys, such as &lt;a href="https://www.yubico.com/products/"&gt;Yubico's YubiKey series&lt;/a&gt; or &lt;a href="https://store.google.com/product/titan_security_key"&gt;Google's Titan keys&lt;/a&gt;, or &lt;a href="https://support.google.com/accounts/answer/9289445"&gt;mobile devices&lt;/a&gt;. These devices make up phishing-resistant MFA. This name was given because the token that is generated for MFA will not work with any site other than the site it was registered with. It's recommended to register at least two keys per account in case you lose one. Some, like Apple, &lt;a href="https://support.apple.com/en-us/HT213154"&gt;require at least two keys&lt;/a&gt;. The U.S. federal government requires phishing-resistant MFA through &lt;a href="https://www.whitehouse.gov/wp-content/uploads/2022/01/M-22-09.pdf#page=4"&gt;the Federal Zero Trust Strategy&lt;/a&gt;. The U.S. National Institute of Standards and Technology (NIST) is recommending phishing-resistant MFA through the &lt;a href="https://doi.org/10.6028/NIST.SP.800-63-4.ipd"&gt;draft version of SP 800-63-4 (Digital Identity Guidelines)&lt;/a&gt;&lt;sup id="fnref5"&gt;5&lt;/sup&gt;. Many companies, including &lt;a href="https://blog.cloudflare.com/how-cloudflare-implemented-fido2-and-zero-trust"&gt;Cloudflare&lt;/a&gt; and &lt;a href="https://twitter.com/frgx/status/1379504541666701313"&gt;Figma&lt;/a&gt;, have mandated FIDO2 security keys because they are phishing-resistant.&lt;/p&gt;

&lt;p&gt;In the event that the site doesn't offer FIDO2 or you don't have a FIDO2-capable device, the other recommended method is &lt;a href="https://www.twilio.com/docs/glossary/totp"&gt;time-based one-time password (TOTP)&lt;/a&gt;, popularized by Google Authenticator. Nowadays, there are countless apps that can generate TOTP, including &lt;a href="https://9to5mac.com/2022/03/07/use-ios-15-2fa-code-generator-plus-autofill-iphone/"&gt;iOS/iPadOS' built-in authenticator&lt;/a&gt; (I personally only recommend using it if you only use iOS/iPadOS and Safari on macOS), &lt;a href="https://authy.com/"&gt;Authy&lt;/a&gt; (a cross-platform synced authenticator), and &lt;a href="https://support.google.com/accounts/answer/1066447"&gt;Google Authenticator&lt;/a&gt;. I personally use &lt;a href="https://play.google.com/store/apps/details?id=me.jmh.authenticatorpro"&gt;Authenticator Pro&lt;/a&gt; (an &lt;a href="https://github.com/jamie-mh/AuthenticatorPro"&gt;open-source&lt;/a&gt; authenticator for Android) on my Android devices, and &lt;a href="https://apps.apple.com/app/otp-auth/id659877384"&gt;OTP Auth&lt;/a&gt; on my iOS devices. Most password managers offer to store TOTP keys in their vaults for you, but that means that all your eggs are in one basket.&lt;/p&gt;

&lt;p&gt;I try to avoid proprietary MFA solutions like Symantec VIP, which a lot of U.S. financial institutions love&lt;sup id="fnref6"&gt;6&lt;/sup&gt;. Instead, and only for those services, I use SMS MFA. One big problem with SMS MFA is if you travel, you don't necessarily have access to your texts. To prevent losing access to those services that use SMS, you could use a service like &lt;a href="https://voice.google.com"&gt;Google Voice&lt;/a&gt; as the phone number for MFA codes.&lt;/p&gt;

&lt;p&gt;If you're curious about what MFA options a service offers, check out &lt;a href="https://2fa.directory"&gt;2FA Directory&lt;/a&gt;&lt;sup id="fnref7"&gt;7&lt;/sup&gt;, a directory of sites that support, and don't support, MFA. If a service is not there, add it by following &lt;a href="https://github.com/2factorauth/twofactorauth/blob/master/CONTRIBUTING.md"&gt;our contribution guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reviewing Account Access
&lt;/h2&gt;

&lt;p&gt;Have you ever been asked to sign in with Google, Twitter, Apple, Facebook, or some other social platform? When you do that, those platforms share some data. For example, with Google, it might be allowing an app to access your Google Drive to store configuration&lt;sup id="fnref8"&gt;8&lt;/sup&gt;. Or with Twitter, an app may want the ability to view Tweets that you've posted and accounts you follow. It's a good practice to periodically go through your platform's security dashboards to review what apps have access to what. The table below contains links to the dashboards of the most common platforms. If you don't recognize an app or device linked to your account, remove it.&lt;/p&gt;

&lt;p&gt;In addition to checking what apps are linked to your account, make sure to review what devices are currently logged in to your account.&lt;/p&gt;

&lt;p&gt;| Platform | App Access                                                                                                   | Logged-in Devices                                                                                  | |----------|--------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------| | Apple    | &lt;a href="https://appleid.apple.com/account/manage/section/security"&gt;Sign-In and Security&lt;/a&gt; (under Sign in with Apple) | &lt;a href="https://appleid.apple.com/account/manage/section/devices"&gt;Devices&lt;/a&gt;                                | | Facebook | &lt;a href="https://www.facebook.com/settings?tab=applications"&gt;Apps and Websites&lt;/a&gt;                                      | &lt;a href="https://accountscenter.facebook.com/password_and_security/login_activity"&gt;Where you're logged in&lt;/a&gt; | | Google   | &lt;a href="https://myaccount.google.com/permissions"&gt;Apps with access to your account&lt;/a&gt;                                 | &lt;a href="https://myaccount.google.com/device-activity"&gt;Your devices&lt;/a&gt;                                       | | Twitter  | &lt;a href="https://twitter.com/settings/connected_apps"&gt;Connected Apps&lt;/a&gt;                                                | &lt;a href="https://twitter.com/settings/sessions"&gt;Sessions&lt;/a&gt;                                                  |&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy Rights
&lt;/h2&gt;

&lt;p&gt;If you're in the &lt;a href="https://wikipedia.org/wiki/European_Economic_Area"&gt;European Economic Area&lt;/a&gt;, Canada, the U.S. states of California, Colorado, Utah, Virginia, or another area that has &lt;a href="https://www.thalesgroup.com/en/markets/digital-identity-and-security/government/magazine/beyond-gdpr-data-protection-around-world"&gt;comprehensive data privacy legislation&lt;/a&gt;, you have a few rights. EEA residents are protected by &lt;a href="https://edps.europa.eu/data-protection/our-work/subjects/rights-individual"&gt;the General Data Protection Regulation (GDPR)&lt;/a&gt;, Canadian residents are protected by &lt;a href="https://www.priv.gc.ca/en/privacy-topics/privacy-laws-in-canada/the-personal-information-protection-and-electronic-documents-act-pipeda/"&gt;the Personal Information Protection and Electronic Documents Act (PIPEDA)&lt;/a&gt;, California residents are covered by &lt;a href="https://cppa.ca.gov/regulations/"&gt;the California Consumer Privacy Act (CCPA) and the California Privacy Rights Act (CPRA)&lt;/a&gt;, and so on, so forth. These regulations give individuals the right to know what personal information an entity&lt;sup id="fnref9"&gt;9&lt;/sup&gt; collects, the right to delete personal information collected, the right to opt-out of the sale of your personal information, and the right to not be discriminated against for exercising your rights.&lt;/p&gt;

&lt;p&gt;A full list of rights can be found by clicking on the corresponding legislation from the following non-exhaustive list of links: &lt;a href="https://edps.europa.eu/data-protection/our-work/subjects/rights-individual"&gt;GDPR (EEA)&lt;/a&gt;, &lt;a href="https://www.priv.gc.ca/en/privacy-topics/privacy-laws-in-canada/the-personal-information-protection-and-electronic-documents-act-pipeda/pipeda_brief/"&gt;PIPEDA (Canada)&lt;/a&gt;, &lt;a href="https://oag.ca.gov/privacy/ccpa"&gt;CCPA/CPRA (California)&lt;/a&gt;, &lt;a href="https://coag.gov/resources/colorado-privacy-act/"&gt;CPA (Colorado)&lt;/a&gt;, &lt;a href="https://usercentrics.com/knowledge-hub/utah-consumer-privacy-act-ucpa/#consumer-rights-under-the-utah-consumer-privacy-act"&gt;UCPA (Utah)&lt;/a&gt;, and &lt;a href="https://www.oag.state.va.us/consumer-protection/files/tips-and-info/Virginia-Consumer-Data-Protection-Act-Summary-2-2-23.pdf"&gt;VCDPA (Virginia)&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;If you have any questions or need any help, feel free to contact me on &lt;a href="https://twitter.com/hkamran80"&gt;Twitter&lt;/a&gt; or &lt;a href="https://vmst.io/@hkamran"&gt;Mastodon&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have any improvements to any of my articles or notes, please &lt;a href="https://github.com/hkamran80/articles#contributions"&gt;submit a pull request&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thank you for reading!&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://www.statista.com/statistics/1305713/average-character-length-of-a-password-us/"&gt;Average number of characters for a password in the United States in 2021 — Statista&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;a href="https://hivesystems.io/password"&gt;Are Your Passwords in the Green? — Hive Systems&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;For more information, check out &lt;a href="https://www.troyhunt.com/heres-how-i-verify-data-breaches/"&gt;Troy's post on how he verifies data breaches&lt;/a&gt; before putting them in HIBP ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;PII is defined as "information that can be used to distinguish or trace an individual’s identity" (source: &lt;a href="https://www.gsa.gov/reference/gsa-privacy-program/rules-and-policies-protecting-pii-privacy-act"&gt;U.S. General Services Administration&lt;/a&gt;). Some examples of PII include your full name, your national ID number/Social Security Number/your country's equivalent, financial account numbers (including credit and debit card numbers), address, phone number, and more. A more comprehensive list can be found on &lt;a href="https://matomo.org/personally-identifiable-information-guide-list-of-pii-examples/"&gt;Matomo Analytics's website&lt;/a&gt;. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;NIST's Digital Identity Guidelines provides technical requirements for U.S. federal agencies implementing account services (source: &lt;a href="https://www.nist.gov/special-publication-800-63"&gt;NIST&lt;/a&gt;). While intended for federal agencies, many companies follow the guidelines or use them as a guideline. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;U.S. financial institutions, please follow &lt;a href="https://investor.vanguard.com/security-center"&gt;Vanguard's example&lt;/a&gt; and support FIDO2! ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;Full disclosure: I am a maintainer of the site ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn8"&gt;
&lt;p&gt;If an app is asking to access your entire Google Drive, check if it really needs that permission. For example, a game requesting that permission probably doesn't need that, and is using it for promotional or other purposes. Google offers developers a scoped permission that limits an app's access to a dedicated folder created just for them in your Google Drive. You might see this permission as "View and manage its own configuration in your Google Drive", or something similar. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn9"&gt;
&lt;p&gt;The definition of an entity depends on the legislation. For example, under the GDPR, entities refer to "a company or entity which processes personal data as part of the activities of one of its branches established in the EU, regardless of where the data is processed; or a company established outside the EU and is offering goods/services (paid or for free) or is monitoring the behaviour of individuals in the EU" (source: &lt;a href="https://commission.europa.eu/law/law-topic/data-protection/reform/rules-business-and-organisations/application-regulation/who-does-data-protection-law-apply"&gt;Who does the data protection law apply to?&lt;/a&gt;). For another example, under the PIPEDA, entities are defined as "private-sector organizations across Canada that collect, use or disclose personal information in the course of a commercial activity" (source: &lt;a href="https://www.priv.gc.ca/en/privacy-topics/privacy-laws-in-canada/the-personal-information-protection-and-electronic-documents-act-pipeda/pipeda_brief/"&gt;PIPEDA in brief&lt;/a&gt;). For one last example, under the CCPA/CPRA, an entity is defined as a for-profit business that meets any of the following: have a gross annual revenue of over $25 million; buy, sell, or share the personal information of 100,000 or more California residents, households, or devices; or derive 50% or more of their annual revenue from selling California residents’ personal information (source: &lt;a href="https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=CIV&amp;amp;sectionNum=1798.140."&gt;Cal Civ. Code § 1798.140(d)&lt;/a&gt;). ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>security</category>
      <category>guide</category>
    </item>
    <item>
      <title>Installing a Newer Version of Python on Amazon EC2</title>
      <dc:creator>H. Kamran</dc:creator>
      <pubDate>Wed, 03 May 2023 16:37:28 +0000</pubDate>
      <link>https://dev.to/hkamran/installing-a-newer-version-of-python-on-amazon-ec2-1b4n</link>
      <guid>https://dev.to/hkamran/installing-a-newer-version-of-python-on-amazon-ec2-1b4n</guid>
      <description>&lt;p&gt;I recently worked on a Python project that used Python 3.11 and Poetry. When I went to deploy it on &lt;a href="https://aws.amazon.com/ec2"&gt;Amazon's Elastic Compute Cloud&lt;/a&gt;, better known by its moniker EC2, I ran into a major hurdle. EC2, running Amazon Linux 2, had Python 3.7.11. The project required 3.11, so I needed to compile Python from source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependencies
&lt;/h2&gt;

&lt;p&gt;I started by updating packages in &lt;a href="https://en.wikipedia.org/wiki/Yum_(software)"&gt;&lt;code&gt;YUM&lt;/code&gt;&lt;/a&gt;, the package manager for Amazon Linux 2, with &lt;code&gt;sudo yum update&lt;/code&gt;. Then I installed some development dependencies, namely the aptly-named "Development Tools" group and a few others with the following command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;yum groupinstall &lt;span class="s2"&gt;"Development Tools"&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;yum &lt;span class="nb"&gt;install &lt;/span&gt;libffi-devel bzip2-devel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One more dependency is needed: &lt;a href="https://en.wikipedia.org/wiki/OpenSSL"&gt;OpenSSL&lt;/a&gt;. The &lt;code&gt;openssl-devel&lt;/code&gt; package included in the repositories with Amazon Linux 2 is 1.0.7, but Python 3 currently requires 1.1.1 or newer. This version is available through the &lt;code&gt;openssl11-devel&lt;/code&gt; package. To install it, uninstall &lt;code&gt;openssl-devel&lt;/code&gt;, then install &lt;code&gt;openssl11-devel&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;yum uninstall openssl-devel
&lt;span class="nb"&gt;sudo &lt;/span&gt;yum &lt;span class="nb"&gt;install &lt;/span&gt;openssl11-devel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Compilation
&lt;/h2&gt;

&lt;p&gt;With the development dependencies installed, the next step was to download the Python source code. The &lt;a href="https://www.python.org/psf-landing/"&gt;Python Software Foundation&lt;/a&gt; maintains tarballs of every Python version released, and they're available through &lt;a href="https://www.python.org/downloads/source/"&gt;this user-friendly page&lt;/a&gt; or &lt;a href="https://www.python.org/ftp/python/"&gt;their plain directory listing&lt;/a&gt;. Either way, find the Python version your project needs, then copy the link to the file that ends in &lt;code&gt;.tgz&lt;/code&gt;, which is the gzipped tarball. With the link to the Python source in hand, download it with &lt;code&gt;wget&lt;/code&gt;, then extract the archive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;wget https://www.python.org/ftp/python/3.11.1/Python-3.11.1.tgz
&lt;span class="nb"&gt;sudo tar &lt;/span&gt;xzf Python-3.11.1.tgz
&lt;span class="nb"&gt;cd &lt;/span&gt;Python-3.11.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, run the &lt;code&gt;configure&lt;/code&gt; script, which will check to ensure that the necessary dependencies are installed: &lt;code&gt;sudo ./configure --enable-optimizations&lt;/code&gt;. The &lt;code&gt;--enable-optimizations&lt;/code&gt; flag optimizes the binary. After that finishes, there will be a &lt;code&gt;Makefile&lt;/code&gt;. Before proceeding, decide whether this new version should replace the system version, or if it should be installed alongside. I recommend the latter, and that's what I used. If you decided to replace the system version, use &lt;code&gt;sudo make install&lt;/code&gt;. If you decided to install it alongside, use &lt;code&gt;sudo make altinstall&lt;/code&gt;. On my &lt;a href="https://aws.amazon.com/ec2/instance-types/t2/"&gt;t2.micro instance&lt;/a&gt;, it took about 35 minutes to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shell Configuration
&lt;/h2&gt;

&lt;p&gt;Python installs to &lt;code&gt;/usr/local/bin&lt;/code&gt;, but the &lt;code&gt;PATH&lt;/code&gt; doesn't contain it. To add this folder to the &lt;code&gt;PATH&lt;/code&gt;, open the &lt;code&gt;~/.bashrc&lt;/code&gt; file in your favourite text editor and add the following to the end.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/bin:&lt;span class="nv"&gt;$PATH&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload the &lt;code&gt;.bashrc&lt;/code&gt; file by running &lt;code&gt;source ~/.bashrc&lt;/code&gt;, then try opening a Python shell with &lt;code&gt;python3.11&lt;/code&gt;. Replace &lt;code&gt;3.11&lt;/code&gt; with whatever Python version was installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;If you have any questions or need any help, feel free to contact me on &lt;a href="https://twitter.com/hkamran80"&gt;Twitter&lt;/a&gt; or &lt;a href="https://vmst.io/@hkamran"&gt;Mastodon&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have any improvements to any of my articles or notes, please &lt;a href="https://github.com/hkamran80/articles#contributions"&gt;submit a pull request&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thank you for reading!&lt;/p&gt;

</description>
      <category>python</category>
      <category>ec2</category>
      <category>aws</category>
      <category>linux</category>
    </item>
    <item>
      <title>Collecting Beta Testers and Cleaning TestFlight CSVs</title>
      <dc:creator>H. Kamran</dc:creator>
      <pubDate>Wed, 26 Apr 2023 00:03:48 +0000</pubDate>
      <link>https://dev.to/hkamran/collecting-beta-testers-and-cleaning-testflight-csvs-29ek</link>
      <guid>https://dev.to/hkamran/collecting-beta-testers-and-cleaning-testflight-csvs-29ek</guid>
      <description>&lt;p&gt;My friend &lt;a href="https://twitter.com/aheze0"&gt;Andrew Zheng&lt;/a&gt; recently started accepting beta testers for his new app via Google Forms. Forms comes with Google Sheets support built-in, so it's easy to get set up. However, a couple problems quickly surfaced.&lt;/p&gt;

&lt;p&gt;First up, Andrew's form was set up with two fields: name and email.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Email&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Andrew Zheng&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aheze@getfind.app&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Joe Smith&lt;/td&gt;
&lt;td&gt;&lt;code&gt;joe@smith.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;TestFlight requires a CSV with three columns: first name, last name, and email.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;First Name&lt;/th&gt;
&lt;th&gt;Last Name&lt;/th&gt;
&lt;th&gt;Email&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Andrew&lt;/td&gt;
&lt;td&gt;Zheng&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aheze@getfind.app&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Joe&lt;/td&gt;
&lt;td&gt;Smith&lt;/td&gt;
&lt;td&gt;&lt;code&gt;joe@smith.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The data needs to be converted first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing the Responses
&lt;/h2&gt;

&lt;p&gt;To convert the two-column data to the three-column CSV, we first created a new sheet. We placed headers at the top for our own reference, First Name, Last Name, and Email, then created a formula to extract each part from the first sheet. The first sheet, named "Testers", contained all the responses from the form. To split the name, we started by using the &lt;code&gt;SEARCH&lt;/code&gt; function, the Google Sheet/Excel equivalent of &lt;code&gt;index&lt;/code&gt;/&lt;code&gt;indexOf&lt;/code&gt;/&lt;code&gt;index(where:)&lt;/code&gt;, to check if there was a space in the name that we could use to split: &lt;code&gt;SEARCH(" ", Testers!$B2)&lt;/code&gt;. If there was, the formula used the &lt;code&gt;LEFT&lt;/code&gt; function to create a substring up to that space:&lt;br&gt;
&lt;code&gt;LEFT(Testers!$B2, SEARCH(" ", Testers!$B2))&lt;/code&gt;. If not, it returned the original content of the cell. The full formula for the first name column is as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=IF(ISNUMBER(SEARCH(" ", Testers!$B2)), LEFT(Testers!$B2, SEARCH(" ", Testers!$B2)), Testers!$B2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last name column has a similar formula, with two differences. The first is that the &lt;code&gt;RIGHT&lt;/code&gt; function is used instead of &lt;code&gt;LEFT&lt;/code&gt;. &lt;code&gt;RIGHT&lt;/code&gt; returns a substring starting&lt;br&gt;
from the end of the string. Because the &lt;code&gt;RIGHT&lt;/code&gt; function is used, the &lt;code&gt;SEARCH&lt;/code&gt; call inside it has to be subtracted from the total length of the string: &lt;code&gt;LEN(Testers!$B2) - SEARCH(" ", Testers!$B2)&lt;/code&gt;. The full formula for the last name column is below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=IF(ISNUMBER(SEARCH(" ", Testers!$B2)), RIGHT(Testers!$B2, LEN(Testers!$B2) - SEARCH(" ", Testers!$B2)), Testers!$B2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The email formula is very simple, just a reference to the cell in the responses sheet: &lt;code&gt;=Testers!$C2&lt;/code&gt;. The three formulas are copied to all the rows in the sheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaning the Responses
&lt;/h2&gt;

&lt;p&gt;The next step was to upload it to TestFlight, but TestFlight threw an error with the most helpful message ever: "An error has occurred. Try again later." Apple, being its usual helpful self, opted to not provide any useful information that could help fix the errors with the CSV. After much trial and error, my friend was able to successfully import his CSV. But the amount of work it took to hunt down errors in a CSV led me to create &lt;a href="https://dev.to/program/testflight-cleaner"&gt;TestFlight Cleaner&lt;/a&gt;. TestFlight Cleaner cleans your tester CSVs for you, and optionally shows why it's removing some entries.&lt;/p&gt;

&lt;h2&gt;
  
  
  TestFlight Cleaner
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ike8m778--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.hkamran.com/images/article/testflight-testers-header" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ike8m778--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.hkamran.com/images/article/testflight-testers-header" alt="TestFlight Cleaner Header" width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first thing I had to do was figure out how to import a CSV and parse it using JavaScript. After reading through Stack Overflow, I stumbled upon the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/FileReader"&gt;FileReader&lt;/a&gt; API, which has a convenient &lt;code&gt;readAsText&lt;/code&gt; function. Since my website uses Next.js (which uses React), I used a &lt;a href="https://react.dev/reference/react/useState"&gt;&lt;code&gt;useState&lt;/code&gt; hook&lt;/a&gt; to store the &lt;code&gt;File&lt;/code&gt; object that the file input would provide, then I accessed that in a function. The function converts the file to text using the aforementioned function, then begins to process the CSV.&lt;/p&gt;

&lt;p&gt;The first step is to turn the raw data into a two-dimensional array. The CSV text was split using the newline character &lt;code&gt;\n&lt;/code&gt;, then each row was split using a comma as a delimiter. This output was stored in another &lt;code&gt;useState&lt;/code&gt;&lt;br&gt;
hook.&lt;/p&gt;

&lt;p&gt;The next step is triggered via a &lt;a href="https://react.dev/reference/react/useEffect"&gt;&lt;code&gt;useEffect&lt;/code&gt; hook&lt;/a&gt; monitoring the &lt;code&gt;csvData&lt;/code&gt; &lt;code&gt;useState&lt;/code&gt; hook, which is set by the &lt;code&gt;processCsv&lt;/code&gt; function in the previous step. This step is the error-checking step. The first check is &lt;a href="https://github.com/hkamran80/website/blob/1a495839379cec3bbae56ec499ad4feba5cde6eb/pages/program/testflight-cleaner.tsx#L65-L76"&gt;a column count&lt;/a&gt;. If it detects more or less than three columns, the step fails, and the user is prompted to fix the CSV. The other error is &lt;a href="https://github.com/hkamran80/website/blob/1a495839379cec3bbae56ec499ad4feba5cde6eb/pages/program/testflight-cleaner.tsx#L78-L94"&gt;a malformed email check&lt;/a&gt;, which checks if an email contains an &lt;code&gt;@&lt;/code&gt; sign and matches &lt;a href="https://github.com/hkamran80/website/blob/1a495839379cec3bbae56ec499ad4feba5cde6eb/pages/program/testflight-cleaner.tsx#L57"&gt;a comprehensive regular expression&lt;/a&gt;. If this check fails, it lets the user know and continues, because it can bypass these rows. Finally, another &lt;code&gt;useState&lt;/code&gt; hook is used to inform another &lt;code&gt;useEffect&lt;/code&gt; hook that this step is complete.&lt;/p&gt;

&lt;p&gt;The third step is to clean the CSV using a reducer. There are two settings the user can apply that affect this function: specifying the first row as the header row, and leaving malformed/duplicated rows in the preview.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detecting Duplicates
&lt;/h3&gt;

&lt;p&gt;The first part of this step is to check if the email has appeared in any preceding rows. If it has and the leave duplicated rows setting is active, it outputs a "duplicate" flag, and adds it to the array. If it has and the setting is inactive, it returns the existing array.&lt;/p&gt;

&lt;h3&gt;
  
  
  Checking for Malformed Emails
&lt;/h3&gt;

&lt;p&gt;The next part is to check for malformed emails, achieved by performing a similar check as step two. If the leave malformed rows setting is active, it outputs a "malformed" flag, then adds it to the array. If neither of these parts are triggered, the reducer does the regular invalid character cleaning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleaning Up Invalid Characters
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/hkamran80/website/blob/1a495839379cec3bbae56ec499ad4feba5cde6eb/pages/program/testflight-cleaner.tsx#L99-L100"&gt;cleaning&lt;/a&gt; process strips all characters that aren't alphanumeric, periods, dashes, spaces or non-English characters from the first and last names. The email gets line break characters stripped. Finally, the function sets a &lt;code&gt;useState&lt;/code&gt; hook with the cleaned data, and another &lt;code&gt;useState&lt;/code&gt; hook with any duplicated emails. The duplicated emails hook is set only if the leave duplicated rows setting is active.&lt;/p&gt;

&lt;p&gt;The final step is to make the preview. It loops over the two-dimensional cleaned CSV data to create a table, and if a malformed or duplicate flag is set, it places a colour on the email. Red signifies malformed and yellow signifies duplicated.&lt;/p&gt;

&lt;p&gt;After a user requests an export using the button below the preview, a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob"&gt;&lt;code&gt;Blob&lt;/code&gt;&lt;/a&gt; is created by iterating over the cleaned rows to form it back into a CSV. An &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a"&gt;anchor element&lt;/a&gt; is created, with its URL set to the output of&lt;br&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL"&gt;&lt;code&gt;URL.createObjectURL&lt;/code&gt;&lt;/a&gt;. A download attribute is also set, which sets the filename to "TestFlight Testers - Cleaned.csv". This element is then added to the DOM, clicked, then removed. The object URL is also revoked.&lt;/p&gt;

&lt;p&gt;I hope that this tool comes in handy for you. If it does or you have any feedback, please contact me on &lt;a href="https://twitter.com/hkamran80"&gt;Twitter&lt;/a&gt; or &lt;a href="https://vmst.io/@hkamran"&gt;Mastodon&lt;/a&gt;. Thank you for reading!&lt;/p&gt;

</description>
      <category>testflight</category>
      <category>csv</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Adding Wildcard Subdomain Support to macOS</title>
      <dc:creator>H. Kamran</dc:creator>
      <pubDate>Mon, 16 Jan 2023 05:36:28 +0000</pubDate>
      <link>https://dev.to/hkamran/adding-wildcard-subdomain-support-to-macos-5bae</link>
      <guid>https://dev.to/hkamran/adding-wildcard-subdomain-support-to-macos-5bae</guid>
      <description>&lt;p&gt;For several years now, I've been using IP addresses and ports to access services that I run on my home server. However, I decided it was time to switch to using a domain instead. I had heard about &lt;a href="https://traefik.io/traefik/"&gt;Traefik&lt;/a&gt; and &lt;a href="https://caddyserver.com/"&gt;Caddy&lt;/a&gt;&lt;br&gt;
in &lt;a href="https://www.reddit.com/r/HomeServer/"&gt;r/HomeServer&lt;/a&gt; and r/homelab](&lt;a href="https://www.reddit.com/r/homelab/"&gt;https://www.reddit.com/r/homelab/&lt;/a&gt;) and chose to try out Traefik, mainly because it had native support for Docker labels.&lt;/p&gt;

&lt;p&gt;The first few steps involved installing Traefik with Docker, adding HTTPS support through Let's Encrypt, and reconfiguring my web containers to use it. The next step was to add a &lt;a href="https://en.wikipedia.org/wiki/Dnsmasq"&gt;dnsmasq&lt;/a&gt; rule to my &lt;a href="https://pi-hole.net/"&gt;Pi-hole&lt;/a&gt; so that all requests would be redirected to the subdomain I chose (I used &lt;code&gt;servername.mydomain.com&lt;/code&gt; as the base host in Traefik). However, on my Mac, I'm not always using my Pi-hole's DNS, usually when I need to circumvent the ad-blocking. I accomplish this through macOS' native support for &lt;a href="https://support.apple.com/en-us/HT202480"&gt;network locations&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After doing a bit of research, I found out I could use dnsmasq on my Mac to do accomplish wildcard subdomains, just like I did on the Pi-hole, and it worked wonderfully once I got it to work.&lt;/p&gt;

&lt;p&gt;I used &lt;a href="https://brew.sh/"&gt;Homebrew&lt;/a&gt; to install and configure dnsmasq: &lt;code&gt;brew install dnsmasq&lt;/code&gt;. I then opened the configuration file for dnsmasq from its place in &lt;code&gt;/usr/local/etc/dnsmasq.conf&lt;/code&gt; and added the line below. If you changed the location of your Homebrew install, run &lt;code&gt;brew --prefix&lt;/code&gt;, then replace &lt;code&gt;/usr/local&lt;/code&gt; with the value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;address=/servername.mydomain.com/10.90.100.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This line configures the routing, and redirects all requests made to &lt;code&gt;servername.mydomain.com&lt;/code&gt; and its subdomains to &lt;code&gt;10.90.100.1&lt;/code&gt;. Make sure to replace &lt;code&gt;10.90.100.1&lt;/code&gt; with the IP you want to redirect to. If you're interested in knowing why dnsmasq redirects all redirects to both the base domain and its subdomains to the IP, &lt;a href="https://stackoverflow.com/a/37449551/7313822"&gt;this Stack Overflow answer&lt;/a&gt; explains why.&lt;/p&gt;

&lt;p&gt;To start dnsmasq, run &lt;code&gt;sudo brew services start dnsmasq&lt;/code&gt;. Homebrew will handle autostarting the daemon and ensuring it stays alive. I found this to be simpler than the common method of copying the Homebrew launch daemon &lt;a href="https://en.wikipedia.org/wiki/Property_list"&gt;plist&lt;/a&gt; to &lt;code&gt;/Library/LaunchDaemons&lt;/code&gt; and manually telling &lt;code&gt;launchctl&lt;/code&gt; to load it.&lt;/p&gt;

&lt;p&gt;If you were to try running a command such as &lt;code&gt;ping&lt;/code&gt; to see if your wildcard subdomain redirection worked, you'd be sorely disappointed, because there are two more steps. Create a folder in &lt;code&gt;/etc&lt;/code&gt; named &lt;code&gt;resolver&lt;/code&gt;, and place a file in there. The file can be named anything, but I like to use the hostname of my computer. In that file, add &lt;code&gt;nameserver 127.0.0.1&lt;/code&gt;. This tells macOS' DNS resolution to use your new dnsmasq as a DNS server. For example, I would run &lt;code&gt;sudo mkdir /etc/resolver&lt;/code&gt;, then &lt;code&gt;sudo bash -c 'echo "nameserver 127.0.0.1" &amp;gt; /etc/resolver/my-computer-hostname'&lt;/code&gt;. Verify that the entry was added with &lt;code&gt;scutil --dns&lt;/code&gt;. An output similar to the following should be shown.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resolver #8
  domain   : my-computer-hostname
  nameserver[0] : 127.0.0.1
  flags    : Request A records, Request AAAA records
  reach    : 0x00030002 (Reachable,Local Address,Directly Reachable Address)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, add the DNS server to macOS via the Network panel in System Preferences or the Wi-Fi panel in System Settings. If your Mac is running Ventura or newer, click "Details", otherwise click "Advanced". Then, navigate to the "DNS" tab. Click the plus button and type in &lt;code&gt;127.0.0.1&lt;/code&gt; and then hit enter. For more information on &lt;code&gt;127.0.0.1&lt;/code&gt;, check out &lt;a href="https://en.wikipedia.org/wiki/Localhost"&gt;its Wikipedia article&lt;/a&gt;. Back to System Preferences, click the OK button, then click Apply. If you're running Ventura or newer, just click the OK button and it will save and apply the settings. Once the icon is disabled and greyed out, flush your DNS cache. You can do that with &lt;code&gt;sudo dscacheutil -flushcache&lt;/code&gt; and &lt;code&gt;sudo killall -HUP mDNSResponder&lt;/code&gt;. Now, try your &lt;code&gt;ping&lt;/code&gt; command again, and you should get a response. If it does, try accessing the service through your browser.&lt;/p&gt;

&lt;p&gt;If you have any questions or need any help, feel free to contact me on &lt;a href="https://twitter.com/hkamran80"&gt;Twitter&lt;/a&gt; or &lt;a href="https://vmst.io/@hkamran"&gt;Mastodon&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I hope that this article is useful for you! Thank you for reading!&lt;/p&gt;

</description>
      <category>macos</category>
      <category>networking</category>
    </item>
    <item>
      <title>Generate TypeScript Declaration Files for JavaScript Files</title>
      <dc:creator>H. Kamran</dc:creator>
      <pubDate>Mon, 09 May 2022 19:44:17 +0000</pubDate>
      <link>https://dev.to/hkamran/generate-typescript-declaration-files-for-javascript-files-1dij</link>
      <guid>https://dev.to/hkamran/generate-typescript-declaration-files-for-javascript-files-1dij</guid>
      <description>&lt;p&gt;I've been moving a few &lt;a href="https://github.com/hkamran80/utilities-js"&gt;utilities&lt;/a&gt; that I use in multiple projects to npm libraries. But I needed an easy, reliable way to generate TypeScript declarations, since I primarily use TypeScript.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Open your project and ensure you have a &lt;code&gt;package.json&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install the &lt;code&gt;typescript&lt;/code&gt; library as a development dependency&lt;br&gt;
 &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With &lt;code&gt;pnpm&lt;/code&gt;: &lt;code&gt;pnpm i -D typescript&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;npm&lt;/code&gt;: &lt;code&gt;npm i -D typescript&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;yarn&lt;/code&gt;: &lt;code&gt;yarn add typescript -D&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add &lt;a href="https://jsdoc.app/"&gt;JSDoc tags&lt;/a&gt; to your functions, variables, classes, etc.&lt;/p&gt;

&lt;p&gt;For example, here's a snippet from &lt;a href="https://www.npmjs.com/package/@hkamran/utility-web"&gt;one of my utilities&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
* Apply classes that result in a true condition
* @param {string[]} classes
* @returns A list of classes
*
* @example
* classNames("block truncate", selected ? "font-medium" : "font-normal")
*/&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;classNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt; &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add the &lt;a href="https://docs.npmjs.com/cli/v8/using-npm/scripts#life-cycle-scripts"&gt;&lt;code&gt;prepare&lt;/code&gt; script&lt;/a&gt; (or whichever one you want to use) to the &lt;code&gt;scripts&lt;/code&gt; object in &lt;code&gt;package.json&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For example, mine looks like this:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"prepare"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsc --declaration --emitDeclarationOnly --allowJs index.js"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt; &lt;/p&gt;

&lt;p&gt;This command runs &lt;code&gt;tsc&lt;/code&gt;, the TypeScript compiler, and tells it to only generate &lt;code&gt;.d.ts&lt;/code&gt; files (declaration files). Be sure to replace &lt;code&gt;index.js&lt;/code&gt; with your JavaScript files.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;prepare&lt;/code&gt; script runs before a npm package is packed (typically with &lt;code&gt;npm publish&lt;/code&gt; or &lt;code&gt;npm pack&lt;/code&gt;, or the equivalents with other package managers).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run your npm script&lt;br&gt;
 &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With &lt;code&gt;pnpm&lt;/code&gt;: &lt;code&gt;pnpm prepare&lt;/code&gt; or &lt;code&gt;pnpm run prepare&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;npm&lt;/code&gt;: &lt;code&gt;npm run prepare&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;yarn&lt;/code&gt;: &lt;code&gt;yarn run prepare&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Using the &lt;code&gt;classNames&lt;/code&gt; function above, the TypeScript compiler generated the following declaration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;classNames&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have any questions, send a tweet my way. I hope that this guide comes in handy for you, thanks for reading!&lt;/p&gt;

</description>
      <category>development</category>
      <category>javascript</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Dark Mode Toggle for Vue.js Apps with Vuetify</title>
      <dc:creator>H. Kamran</dc:creator>
      <pubDate>Thu, 04 Jun 2020 23:55:08 +0000</pubDate>
      <link>https://dev.to/hkamran/using-local-storage-to-store-vuetify-s-dark-theme-state-4e6g</link>
      <guid>https://dev.to/hkamran/using-local-storage-to-store-vuetify-s-dark-theme-state-4e6g</guid>
      <description>&lt;p&gt;&lt;em&gt;Photo: &lt;a href="https://material.io/design/color/dark-theme.html"&gt;Material.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I use &lt;a href="https://vuejs.org"&gt;Vue.js&lt;/a&gt; and &lt;a href="https://vuetifyjs.com"&gt;Vuetify&lt;/a&gt; for almost all of my websites and I’m a huge supporter of dark mode. One of the many reasons I chose Vuetify is because it has dark mode support out-of-the-box. So, without further ado, let me guide you through easily changing the dark mode state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting the Default Dark Mode State
&lt;/h2&gt;

&lt;p&gt;In order to set the default dark mode state, we have to open the plugin file for Vuetify, which is available at &lt;code&gt;src/plugins/vuetify.js&lt;/code&gt;. By default, the file should look like the following.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Vue&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Vuetify&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vuetify/lib&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;Vue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Vuetify&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Vuetify&lt;/span&gt;&lt;span class="p"&gt;({});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To set the default state, we have to create a new object in the constructor called &lt;code&gt;theme&lt;/code&gt;, and inside of that, set a variable called &lt;code&gt;dark&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Vuetify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if we want to change it from the user-facing interface, we have to use the variable provided by Vuetify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting the Dark Mode State From the Interface
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;A copy of the final code is available at the bottom.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before even adding the theme state-changing code, you have to decide where to put the code. You only have to put it in one location, preferably a location that is persistent, such as your &lt;code&gt;App.vue&lt;/code&gt; or a component that is present on all pages, such as a navigation bar. With that decided, we can actually get to work.&lt;/p&gt;

&lt;p&gt;In your file (I’m using a component that I’ve called &lt;code&gt;NavigationBar&lt;/code&gt;), go to the &lt;code&gt;script&lt;/code&gt; tag. There should be an &lt;code&gt;export&lt;/code&gt; statement present. If not, go ahead and create it. The contents of the &lt;code&gt;script&lt;/code&gt; tag should look similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NavigationBar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we need to add the method that will be called when the user clicks on a button. Underneath the &lt;code&gt;name&lt;/code&gt; parameter, add a new object called &lt;code&gt;methods&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NavigationBar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I’m going to call my method &lt;code&gt;toggleDarkMode&lt;/code&gt;, but feel free to call it whatever you’d like. This method is going to set the dark mode variable (&lt;code&gt;this.$vuetify.theme.dark&lt;/code&gt;) to the inverse of what it is currently set to (if the theme is currently light, then this variable will be &lt;code&gt;false&lt;/code&gt;), then set a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage"&gt;local storage&lt;/a&gt; variable called &lt;code&gt;darkTheme&lt;/code&gt; to the value of that variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;toggleDarkMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$vuetify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$vuetify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;darkTheme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$vuetify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the function implemented, we now have to make it so that site will automatically pick up the theme state from the browser with the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme"&gt;&lt;code&gt;prefers-color-scheme&lt;/code&gt; CSS media query&lt;/a&gt; and/or the local storage state. The &lt;code&gt;prefers-color-scheme&lt;/code&gt; state is set by your system.&lt;/p&gt;

&lt;p&gt;To accomplish our task, we will use a &lt;a href="https://michaelnthiessen.com/call-method-on-page-load/"&gt;Vue lifecycle hook&lt;/a&gt; called &lt;code&gt;mounted&lt;/code&gt; which is called, as you may have guessed, when the component is mounted. We’ll add &lt;code&gt;mounted() {}&lt;/code&gt; underneath the &lt;code&gt;methods&lt;/code&gt; object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NavigationBar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will first check what the value of our local storage variable is. If it exists, &lt;code&gt;this.$vuetify.theme.dark&lt;/code&gt; is set to the value of the variable. If it doesn’t, we’ll check whether the user has dark mode enabled on their system, and set it to that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;darkTheme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Check if the user has set the theme state before&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$vuetify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$vuetify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matchMedia&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(prefers-color-scheme: dark)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$vuetify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;darkTheme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$vuetify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All that’s left is to add a button to toggle the state. In the &lt;code&gt;template&lt;/code&gt; tag, add the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;v-btn&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"toggleDarkMode"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;v-icon&amp;gt;&lt;/span&gt;mdi-theme-light-dark&lt;span class="nt"&gt;&amp;lt;/v-icon&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/v-btn&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code above is simple. It creates a Vuetify icon button, tells it to use the &lt;a href="https://materialdesignicons.com/icon/theme-light-dark"&gt;&lt;code&gt;theme-light-dark&lt;/code&gt; icon from Material Design Icons&lt;/a&gt; and to add an event handler, which on click, calls the &lt;code&gt;toggleDarkMode&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;That’s it. You’re finished! As I mentioned earlier, the final code is available on &lt;a href="https://gist.github.com/hkamran80/9bba61e1d2f0c2cfae8209e7d8dca4f1"&gt;this GitHub Gist&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>vuetify</category>
      <category>vue</category>
      <category>development</category>
      <category>theming</category>
    </item>
    <item>
      <title>Getting balenaEtcher to Work on macOS Catalina</title>
      <dc:creator>H. Kamran</dc:creator>
      <pubDate>Fri, 31 Jan 2020 01:38:24 +0000</pubDate>
      <link>https://dev.to/hkamran/getting-balenaetcher-to-work-on-macos-catalina-koe</link>
      <guid>https://dev.to/hkamran/getting-balenaetcher-to-work-on-macos-catalina-koe</guid>
      <description>&lt;p&gt;I just recently upgraded to macOS Catalina, and needed to flash a microSD card for my Raspberry Pi. However, balenaEtcher was throwing an error when trying to flash the image.&lt;/p&gt;

&lt;p&gt;After a little while, I realized that Catalina included some extra privacy protections. I went into System Preferences and turned on “Full Disk Access” for balenaEtcher. But lo and behold, the error still persisted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;After doing a bit of research, I found that balenaEtcher, and Etcher (the former name of balenaEtcher), had a command-line utility to launch the app.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sudo /Applications/balenaEtcher.app/Contents/MacOS/balenaEtcher&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The command booted up a graphical interface of balenaEtcher, the difference that, because of the “sudo” in the command, it ran under the root user. This bypassed the security/privacy restrictions placed (wisely) by Apple on Catalina.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SUwBz-Ov--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://imgix.cosmicjs.com/f9e82ae0-438b-11ec-9580-ebf669758fed-getting-balenaetcher-to-work-on-macos-catalina-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SUwBz-Ov--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://imgix.cosmicjs.com/f9e82ae0-438b-11ec-9580-ebf669758fed-getting-balenaetcher-to-work-on-macos-catalina-1.png" alt="Image: balenaEtcher after being opened with sudo from the command-line" width="800" height="519"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mac</category>
      <category>macos</category>
      <category>apple</category>
      <category>balenaetcher</category>
    </item>
    <item>
      <title>Extracting PKGs on macOS</title>
      <dc:creator>H. Kamran</dc:creator>
      <pubDate>Thu, 30 Jan 2020 14:31:01 +0000</pubDate>
      <link>https://dev.to/hkamran/extracting-pkgs-on-macos-4g6a</link>
      <guid>https://dev.to/hkamran/extracting-pkgs-on-macos-4g6a</guid>
      <description>&lt;p&gt;&lt;em&gt;Photo by: &lt;a href="https://unsplash.com/@schwiet?utm_source=hkamran&amp;amp;utm_medium=referral"&gt;seth schwiet&lt;/a&gt; on &lt;a href="https://unsplash.com?utm_source=hkamran&amp;amp;utm_medium=referral"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's say you're on a computer where you don’t have administrative access, but you really need to use this one piece of software. In my case, this was Apple’s SF Symbols app. There’s a pretty simple way to extract the payload from the package (.PKG).&lt;/p&gt;

&lt;p&gt;To get started, you first need two things. A macOS-equipped computer and a DMG with a PKG inside, or just a PKG. This tutorial will detail both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extracting the Package Contents
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If your PKG is inside of a DMG, start here&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To extract the payload from a PKG inside of a DMG, we need to mount the DMG. There are two ways to do this. You can either use the Finder (double-click the DMG to mount it) or use the terminal with the following command: &lt;code&gt;hdiutil attach [path to your DMG]&lt;/code&gt;. For example, &lt;code&gt;hdiutil attach ~/Downloads/SF\ Symbols.dmg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PKG Extraction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now is where we have to use the terminal. In your terminal, navigate to the folder just above where you want your PKG to be extracted. An example command is as follows: &lt;code&gt;cd ~/Documents&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can then proceed to extract the PKG with: &lt;code&gt;pkgutil --expand-full [package to PKG] [folder to extract to]&lt;/code&gt;. For example, &lt;code&gt;pkgutil --expand-full /Volumes/SFSymbols/SF\ Symbols.pkg extracted_package&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The contents of the PKG are now available in the &lt;code&gt;extracted_package&lt;/code&gt; folder. With the &lt;code&gt;SF Symbols.pkg&lt;/code&gt; extracted, it has three directories: &lt;code&gt;Distribution&lt;/code&gt; and &lt;code&gt;Resources&lt;/code&gt;, &lt;code&gt;SFSymbols.pkg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The content of the PKG is stored in the (in my case) &lt;code&gt;SFSymbols.pkg&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;The folder hierarchy of the Payload folder (&lt;code&gt;extracted_package/[package name]/Payload&lt;/code&gt;) is very simple. The directories are the places to put the files. For example, the directory &lt;code&gt;Applications&lt;/code&gt; has &lt;code&gt;SF Symbols.app&lt;/code&gt;, because that needs to be put in the Applications folder on your computer.&lt;/p&gt;

&lt;p&gt;Once you have extracted the Payload, you can eject the volume (if you are using a DMG) or delete the PKG, as you no longer need it.&lt;/p&gt;

</description>
      <category>mac</category>
      <category>macos</category>
      <category>apple</category>
    </item>
  </channel>
</rss>
