29 Sep 2024
In any time-sensitive application, such as those relying on SAML authentication, ensuring your server’s time is accurate is critical. A common culprit for time-related issues is clock drift, which happens when your server’s time gradually becomes inaccurate. This blog post will cover how to detect and fix clock drift using NTP (Network Time Protocol), ensuring your system is always in sync with global time standards.
What is Clock Drift?
Clock drift refers to the gradual deviation of a system’s clock from the actual, accurate time. It happens because every system has an internal clock that measures time, and these clocks aren’t always perfectly accurate. The drift may be small, but over time it can accumulate, causing significant discrepancies between your system’s time and the real time.
For example, a system clock might drift a few seconds per day. Over a few weeks, this could turn into minutes or even hours of difference, depending on the system. This can lead to major problems in time-sensitive operations like:
- Authentication (e.g., SAML, OAuth)
- Cache expiration
- Database transactions with time-based queries
- Time-sensitive event scheduling
What is NTP?
NTP (Network Time Protocol) is a protocol used to synchronize your system’s clock with a highly accurate time source, like an atomic clock or a GPS clock. NTP works by periodically querying remote time servers, which provide the correct time, and adjusting the system’s clock as necessary to stay in sync.
How NTP Sync Fixes Clock Drift
NTP fixes clock drift by ensuring that your system regularly checks a reliable time source and corrects its internal clock. Once NTP is installed and configured, your system will constantly adjust its time to stay as close as possible to the global time standard.
Fixing NTP Sync on a System
Here’s how you can check and fix NTP synchronization issues on a typical Linux server running an application.
Step 1: Check Current Time Sync Status
Before making changes, you can check if your system’s time is synchronized by using the following command:
This command will tell you if your system clock is synchronized or if there are issues.
Here’s an example of what you might see:
Local time: Fri 2024-09-30 14:15:02 UTC
Universal time: Fri 2024-09-30 14:15:02 UTC
RTC time: Fri 2024-09-30 14:15:02
Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: no
NTP service: inactive
RTC in local TZ: no
In this case, “System clock synchronized: no” indicates that the system is not currently synchronized, which could lead to clock drift.
Step 2: Install NTP or Chrony
On most Linux distributions, you can use NTP or Chrony to sync your system’s clock. Chrony is often preferred for modern systems because it’s faster and more efficient, but NTP is still widely used.
Installing NTP:
On Ubuntu or Debian systems, install NTP with:
sudo apt update
sudo apt install ntp
For Red Hat or CentOS systems:
Installing Chrony:
If you prefer Chrony instead:
sudo apt install chrony # Ubuntu/Debian
sudo yum install chrony # CentOS/RedHat
Step 3: Enable and Start the NTP Service
Once NTP (or Chrony) is installed, you need to enable and start the service to begin syncing the time.
For NTP:
sudo systemctl enable ntp
sudo systemctl start ntp
For Chrony:
sudo systemctl enable chronyd
sudo systemctl start chronyd
Step 4: Verify Time Synchronization
After starting the service, you can check if the synchronization is working with the following command:
You should now see something like:
Local time: Fri 2024-09-30 14:20:00 UTC
Universal time: Fri 2024-09-30 14:20:00 UTC
RTC time: Fri 2024-09-30 14:20:00
Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no
The key field here is “System clock synchronized: yes”, which indicates that your server’s time is now properly synchronized.
Step 5: Test NTP Sync
To check how your system is performing in terms of time synchronization, you can use the ntpq command to get details about the time servers your system is syncing with:
This will give you a table showing the servers being used for synchronization, along with the delay and offset:
remote refid st t when poll reach delay offset jitter
==============================================================================
*time.nist.gov .NIST. 1 u 23 64 377 52.123 -0.987 1.145
+clock.isc.org .GPS. 1 u 30 64 377 30.245 0.245 0.987
In this table, you want to see low offsets and jitter values, indicating that your system’s clock is close to being perfectly in sync with the external time source.
What Happens if NTP is Not Set Up?
Without NTP, your system will slowly experience clock drift, and over time, the system clock will become inaccurate. This can cause issues in:
- Authentication protocols like SAML, where tokens are only valid for short time windows.
- Scheduled tasks that rely on accurate timing.
- Logging systems, where out-of-sync times can make debugging difficult.
- Database records, where time-sensitive queries (
updated_at, created_at) can lead to inconsistencies.
In extreme cases, significant time discrepancies can cause downtime, particularly for systems dependent on strict time validation.
How Does Clock Drift Occur?
Clock drift happens because no hardware clock is perfect. The reasons include:
-
Hardware limitations: System clocks are built with internal quartz crystals that vibrate to keep time. However, due to imperfections in the crystals, temperature changes, or electrical noise, the vibrations may vary slightly, leading to time drift.
-
System load: If your server is under heavy load, its internal clock may lose time accuracy as the CPU focuses on other tasks.
-
Power fluctuations: Inconsistent power delivery to a system can cause its clock to drift.
Without regular synchronization via NTP, these issues accumulate over time, making the system clock increasingly inaccurate.
Conclusion
Ensuring that your system stays in sync with global time standards using NTP is critical for time-sensitive operations. Whether you’re dealing with authentication systems, database queries, or scheduled tasks, clock drift can cause serious issues if left unchecked. With NTP or Chrony properly configured, your system can automatically correct its clock drift, keeping your applications running smoothly and securely.
25 Sep 2024
Point-free style, also known as tacit programming, is a method of writing functions without explicitly mentioning their arguments. Instead of focusing on what the arguments are, point-free style emphasizes how functions are combined. This style is prominent in functional programming and can be found in many functional languages, such as Haskell and Elixir.
In this blog post, we’ll explore the origins of point-free style, how it’s implemented in Haskell and Elixir, and how these two languages compare when it comes to this technique.
The Origins of Point-Free Style
The roots of point-free style go back to combinatory logic, a branch of mathematical logic developed by Moses Schönfinkel in the 1920s and later extended by Haskell Curry. Combinatory logic aimed to eliminate the need for variables in functions by focusing solely on function composition. This allowed expressions to be more concise and abstract.
The goal of combinatory logic was to describe computations purely through function application and composition without referring to the individual arguments, hence “point-free” (where “points” refer to arguments).
While combinatory logic originated in mathematics, its ideas have strongly influenced functional programming languages. APL, developed in the 1960s, was one of the first languages to use a similar concept, and Miranda, developed in the 1980s, popularized it further. Haskell, born in the 1990s, adopted and refined this approach, making point-free style a hallmark of functional programming.
Point-Free Style in Haskell
Haskell is known for its elegant use of function composition and point-free style. In Haskell, you can define functions by composing other functions, without mentioning the arguments.
Here’s a simple example of a point-free function in Haskell:
makeGreeting = (<>) . (<> " ")
In this case:
- The function
(<>) is the string concatenation operator.
- The expression
(<>) . (<> " ") creates a new function that concatenates a space between two strings.
When you call makeGreeting "hello" "world", it returns "hello world". The magic of point-free style here is that you never explicitly mention the arguments name1 or name2 — everything is done through function composition.
Benefits in Haskell
- Conciseness: Point-free style reduces the verbosity by removing explicit references to arguments.
- Readability (in some cases): For those accustomed to functional programming, this style can make code more expressive and elegant by highlighting the transformations applied to the data rather than focusing on the data itself.
However, point-free style can become unreadable if overused, especially when the composition involves many functions. This is where balance is key.
Point-Free Style in Elixir
Elixir, while also a functional programming language, has a slightly different syntax and approach to anonymous functions compared to Haskell. Elixir doesn’t support point-free style as directly as Haskell, but it still allows for a concise definition of anonymous functions using the capture operator (&).
Here’s the equivalent of the Haskell point-free example in Elixir:
make_greeting = &(&1 <> " " <> &2)
In this Elixir version:
- The
& symbol is a shorthand for anonymous functions.
&1 and &2 refer to the first and second arguments, respectively.
This function behaves the same as its Haskell counterpart: calling make_greeting.("hello", "world") will return "hello world".
Differences in Elixir
Unlike Haskell, Elixir does not emphasize point-free style as much, mainly due to its roots in the Erlang VM (which was not designed with pure functional programming in mind). However, Elixir provides the & capture operator for defining short, anonymous functions in a concise way, which offers some point-free-like benefits.
Example: Haskell vs Elixir
Let’s compare a slightly more complex example in both languages: a function that doubles each element in a list.
- Haskell:
In Haskell, doubleAll is a point-free function that uses map to apply (* 2) to each element in a list. The function is defined entirely without mentioning the list argument.
- Elixir:
double_all = &Enum.map(&1, &(&1 * 2))
In Elixir, while you can write it concisely using the capture operator (&), it’s not as purely point-free as Haskell. Here, you still need to refer to &1 to indicate the argument, and you call the Enum.map/2 function.
Pros and Cons of Point-Free Style
Advantages
- Conciseness: Point-free style leads to more compact code, which can be beneficial when dealing with simple function compositions.
- Abstraction: By focusing on composition rather than individual arguments, the code often becomes more abstract and modular.
- Readability (for small compositions): When used correctly, point-free style can make code more expressive by focusing on the logic of function composition.
Disadvantages
- Readability (for complex compositions): As compositions grow in complexity, point-free style can become difficult to follow, making code harder to understand and maintain.
- Debugging: When errors occur, it can be harder to track down the source, as the explicit argument handling is missing.
Conclusion
Point-free style has its origins in combinatory logic and has found a home in functional programming, particularly in languages like Haskell. Haskell makes point-free style a natural part of its programming model, allowing for elegant function compositions without mentioning arguments.
In contrast, while Elixir supports concise anonymous functions using the capture operator (&), it doesn’t fully embrace point-free style in the same way as Haskell. However, both languages allow for powerful functional programming techniques, and knowing when to use or avoid point-free style can lead to clearer, more maintainable code.
The key takeaway is that point-free style is a useful tool, but like any tool, it should be used judiciously, especially as the complexity of your code grows.
References
I hope this gives you a good overview of point-free style and how it compares between Haskell and Elixir.
16 Sep 2024
One often overlooked aspect of a code base is the commit history.
Enter Commitizen, a powerful tool that standardizes commit messages and automates changelog generation.
In this post, we’ll explore how Commitizen can improve your development workflow and discuss some common commit guidelines.
What is Commitizen?
Commitizen is a command-line utility that helps developers write standardized commit messages. It provides an interactive prompt that guides you through the commit process, ensuring that your commits follow a consistent format.
Installing and Configuring Commitizen
Installation
You can install Commitizen globally using either npm or yarn:
Using npm:
npm install -g commitizen
Using yarn:
yarn global add commitizen
Configuration
To customize Commitizen for your project, you can create a .czrc file in your project root or home directory. Here’s an example of a .czrc file:
{
"path": "cz-conventional-changelog",
"types": {
"feat": {
"description": "A new feature"
},
"fix": {
"description": "A bug fix"
},
"docs": {
"description": "Documentation only changes"
},
"style": {
"description": "Changes that do not affect the meaning of the code"
},
"refactor": {
"description": "A code change that neither fixes a bug nor adds a feature"
},
"perf": {
"description": "A code change that improves performance"
},
"test": {
"description": "Adding missing tests"
},
"chore": {
"description": "Changes to the build process or auxiliary tools"
},
"a11y": {
"description": "Adding accessibility features"
},
"sec": {
"description": "A code change related to security"
},
"devops": {
"description": "A code change related to devops"
}
},
"messages": {
"type": "Select the type of change you're committing:",
"subject": "Write a short, imperative tense description of the change:\n",
"body": "Provide a longer description of the change (optional). Use Markdown for formatting:\n",
"breaking": "List any breaking changes:\n",
"footer": "List any issues closed by this change (optional):\n"
},
"subjectLimit": 100,
"upperCaseSubject": false,
"allowCustomScopes": false,
"allowBreakingChanges": ["feat", "fix"],
"footerPrefix": "ISSUES CLOSED:"
}
This configuration file allows you to customize the commit types, prompts, and other aspects of Commitizen’s behavior. You can adjust these settings to match your project’s specific needs.
For more detailed information on configuration options, you can refer to the Commitizen GitHub documentation.
Benefits of Using Commitizen
- Consistency: Enforces a uniform commit message format across your team.
- Clarity: Makes it easier to understand the purpose of each commit at a glance.
- Automation: Facilitates automatic generation of changelogs.
- Better Code Reviews: Standardized commits make it easier to review code changes.
Common Commit Types
Let’s break down some commonly used commit types and their purposes:
- feat: A new feature
- fix: A bug fix
- docs: Documentation only changes
- style: Changes that do not affect the meaning of the code (e.g., formatting)
- refactor: A code change that neither fixes a bug nor adds a feature
- perf: A code change that improves performance
- test: Adding missing tests
- chore: Changes to the build process or auxiliary tools
- a11y: Adding accessibility features
- sec: A code change related to security
- devops: A code change related to DevOps
Anatomy of a Good Commit Message
A well-structured commit message typically includes:
- Type: The category of the change (e.g., feat, fix, docs)
- Subject: A short, imperative description of the change
- Body (optional): A longer description of the change, using Markdown for formatting
- Breaking Changes (if any): List of any breaking changes introduced
- Footer (optional): References to issues closed by the change
Using Commitizen: A Walkthrough
Let’s walk through the process of creating a commit using Commitizen. After you’ve made your changes and staged them with git add, instead of using git commit, you’ll use git cz. Here’s what the process looks like:
- In your terminal, type:
-
Commitizen will launch and guide you through the commit process:
? Select the type of change you're committing: (Use arrow keys)
❯ feat: A new feature
fix: A bug fix
docs: Documentation only changes
style: Changes that do not affect the meaning of the code
refactor: A code change that neither fixes a bug nor adds a feature
perf: A code change that improves performance
test: Adding missing tests
(Move up and down to reveal more choices)
Use the arrow keys to select the appropriate type and press Enter.
-
Next, you’ll be prompted to enter the scope of the change (optional):
? What is the scope of this change (e.g. component or file name): (press enter to skip)
auth
Enter the scope or press Enter to skip.
-
Now, enter a short description of the change:
? Write a short, imperative tense description of the change:
implement JWT-based user authentication
-
Provide a longer description if needed:
? Provide a longer description of the change: (press enter to skip)
Implement JWT-based authentication for user login and registration.
This includes:
- Creating login and registration endpoints
- Implementing password hashing
- Setting up JWT token generation and validation
-
Indicate if there are any breaking changes:
? Are there any breaking changes? (y/N)
y
If you answer yes, you’ll be prompted to describe the breaking changes:
? Describe the breaking changes:
API now requires authentication for most endpoints
-
Finally, indicate if this change affects any open issues:
? Does this change affect any open issues?
y
If yes, you’ll be prompted to add issue references:
? Add issue references (e.g. "fix #123", "re #123".):
Closes #123, #124
-
Commitizen will then generate the commit message based on your inputs and create the commit:
[master 5c25de1] feat(auth): implement JWT-based user authentication
Implement JWT-based authentication for user login and registration.
This includes:
- Creating login and registration endpoints
- Implementing password hashing
- Setting up JWT token generation and validation
BREAKING CHANGE: API now requires authentication for most endpoints
Closes #123, #124
This process ensures that your commit message is structured consistently and contains all the necessary information. The resulting commit message will look like this:
feat(auth): implement JWT-based user authentication
Implement JWT-based authentication for user login and registration.
This includes:
- Creating login and registration endpoints
- Implementing password hashing
- Setting up JWT token generation and validation
BREAKING CHANGE: API now requires authentication for most endpoints
Closes #123, #124
By following this process, you create commits that are not only informative but also adhere to a consistent format, making it easier for your team to understand changes and automatically generate meaningful changelogs.
Sample Problematic Commit
While Commitizen won’t automatically reject commits, it guides users towards more descriptive messages. Here’s an example of a commit that Commitizen would try to improve:
type: updated stuff
Select the type of change you're committing:
> feat
Write a short, imperative tense description of the change:
> updated stuff
Provide a longer description of the change (optional). Use Markdown for formatting:
>
List any breaking changes:
>
List any issues closed by this change (optional):
>
In this scenario, Commitizen would create a commit message like:
While this commit would be accepted, it still lacks specificity and doesn’t provide useful information about the changes made. Commitizen’s prompts encourage developers to provide more details, but it ultimately depends on the user to input meaningful information.
Generating and Understanding Changelogs
While Commitizen helps in creating structured commit messages, generating changelogs requires additional tools that work well with Commitizen’s commit format. Let’s explore how to generate a changelog and what the result looks like.
Generating a Changelog
Two popular options for generating changelogs are standard-version and conventional-changelog-cli.
Using standard-version
-
Install standard-version:
npm install -g standard-version
-
Generate a changelog:
This command analyzes your commits, updates your version in package.json, generates a changelog, creates a new commit with these changes, and creates a new tag with the new version number.
-
To generate the changelog without bumping the version:
standard-version --skip.bump --skip.tag
Using conventional-changelog-cli
-
Install conventional-changelog-cli:
npm install -g conventional-changelog-cli
-
Generate the changelog:
conventional-changelog -p angular -i CHANGELOG.md -s
-
To generate the entire changelog from scratch:
conventional-changelog -p angular -i CHANGELOG.md -s -r 0
Automating Changelog Generation
Add this script to your package.json to automate changelog generation:
{
"scripts": {
"version": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md"
}
}
Now, running npm version will automatically update your changelog.
Example Changelog
Here’s an example of what a generated changelog might look like when following Commitizen conventions:
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.2.0] - 2024-09-20
### Added
- feat: implement user authentication system (#123)
- Added JWT-based authentication for login and registration
- Created new endpoints for user management
- feat: add dark mode to user interface (#145)
- feat(api): introduce rate limiting for public endpoints (#150)
### Changed
- refactor: optimize database queries for better performance (#135)
- style: update color scheme across the application (#140)
### Fixed
- fix: resolve issue with password reset functionality (#128)
- fix(ui): correct alignment of buttons in mobile view (#142)
### Security
- sec: update dependencies to address known vulnerabilities (#155)
## [1.1.0] - 2024-08-15
### Added
- feat: introduce search functionality for products (#110)
- feat(api): add pagination to list endpoints (#115)
### Changed
- refactor: restructure folder organization for better modularity (#105)
- perf: optimize image loading for faster page renders (#120)
### Fixed
- fix: resolve cart calculation errors (#112)
- fix(ui): correct responsive layout issues on tablet devices (#118)
### Documentation
- docs: update API documentation with new endpoints (#125)
## [1.0.1] - 2024-07-30
### Fixed
- fix: resolve critical bug in payment processing (#102)
- fix(ui): correct typos in error messages (#104)
### Security
- sec: implement additional checks for user input sanitization (#103)
## [1.0.0] - 2024-07-15
### Added
- Initial release of the application
- feat: core e-commerce functionality including product listing, cart, and checkout
- feat: user registration and profile management
- feat: basic reporting and analytics for administrators
### Documentation
- docs: create initial README and contribution guidelines (#95)
- docs: add inline code comments for complex functions (#97)
### DevOps
- devops: set up CI/CD pipeline for automated testing and deployment (#90)
This changelog demonstrates how structured commit messages translate into a clear, informative record of project changes. It’s organized by version, with changes categorized based on their type. Each entry corresponds directly to a commit, making it easy to trace changes back to their source.
The effectiveness of these changelog generation tools depends on the quality and consistency of your commit messages. This is where Commitizen really shines, ensuring that all your commits follow a format that these changelog generators can understand and process.
Conclusion
By adopting Commitizen and following consistent commit guidelines, you can significantly improve your codebase’s maintainability and documentation. While Commitizen provides a framework for better commits, it’s up to the development team to ensure that the content of the commits is meaningful and descriptive.
Remember, good commit messages are a form of documentation. They not only help your team understand changes but also make it easier for future developers (including yourself) to understand the project’s history and evolution. The more specific and descriptive your commit messages are, the more valuable they become in the long run, especially when it comes to generating comprehensive and useful changelogs.
11 Sep 2024
This guide outlines the process of integrating Stripe payments into a Phoenix 1.7 application using the Stripity Stripe library. It covers the key steps in implementation and highlights the differences in Phoenix 1.7’s architecture that may affect the integration process.
Advantages of Stripity Stripe
Stripity Stripe offers several benefits for handling payments in Elixir:
- Elixir-native implementation: Designed specifically for use with Elixir.
- Comprehensive coverage: Supports most features of the Stripe API.
- Regular maintenance: Kept up-to-date with Stripe’s API changes.
- Type safety: Utilizes Elixir’s type system to reduce runtime errors.
Compared to manual API integration, Stripity Stripe simplifies API calls, webhook handling, and error management.
Architectural Changes in Phoenix 1.7
Phoenix 1.7 introduces several changes to the framework’s structure:
- Function Components: Replaces traditional templates with function components in a dedicated HTML module.
- Embedded Templates: Templates are now embedded directly in the HTML module using
embed_templates "*.html".
- Updated Path Helpers: The
~p sigil replaces the previous Routes.x_path syntax.
These changes affect how views and templates are structured and how routes are referenced within the application.
Implementation Steps
1. Project Setup
Add Stripity Stripe to your mix.exs:
{:stripity_stripe, "~> 2.0"}
Run mix deps.get to install the dependency.
2. Stripe Configuration
In config/config.exs, add:
config :stripity_stripe, api_key: System.get_env("STRIPE_SECRET_KEY")
3. Create and Run Migration
Before setting up the controller, we need to create a database table to store payment information. Let’s create a migration:
mix ecto.gen.migration create_payments
This will create a new migration file in the priv/repo/migrations directory. Open the newly created file and add the following content:
defmodule YourApp.Repo.Migrations.CreatePayments do
use Ecto.Migration
def change do
create table(:payments) do
add :amount, :integer
add :stripe_id, :string
add :status, :string
timestamps()
end
create index(:payments, [:stripe_id])
end
end
This migration creates a payments table with fields for the amount, Stripe ID, and status of the payment. The timestamps() function adds inserted_at and updated_at fields.
Now, run the migration:
4. Create Payment Schema
After creating the database table, we need to define a schema for it. Create a new file lib/your_app/payments/payment.ex:
defmodule YourApp.Payments.Payment do
use Ecto.Schema
import Ecto.Changeset
schema "payments" do
field :amount, :integer
field :stripe_id, :string
field :status, :string
timestamps()
end
def changeset(payment, attrs) do
payment
|> cast(attrs, [:amount, :stripe_id, :status])
|> validate_required([:amount, :stripe_id, :status])
end
end
This schema corresponds to the database table we just created and provides a changeset function for validating and casting payment data.
5. Payment Controller
Create lib/your_app_web/controllers/payment_controller.ex:
defmodule YourAppWeb.PaymentController do
use YourAppWeb, :controller
alias YourApp.Payments.Payment
def new(conn, _params) do
changeset = Payment.changeset(%Payment{}, %{})
stripe_publishable_key = Application.get_env(:liftforge, :stripe_publishable_key)
render(conn, :new, changeset: changeset, stripe_publishable_key: stripe_publishable_key)
end
def create(conn, %{"payment" => payment_params}) do
amount = payment_params["amount"]
token = payment_params["token"]
case Stripe.Charge.create(%{
amount: amount,
currency: "usd",
source: token,
description: "Example charge"
}) do
{:ok, charge} ->
{:ok, _payment} =
%Payment{}
|> Payment.changeset(%{amount: amount, stripe_id: charge.id})
|> YourApp.Repo.insert()
conn
|> put_flash(:info, "Payment successful.")
|> redirect(to: ~p"/payment/thank-you?amount=#{amount}")
{:error, error} ->
conn
|> put_flash(:error, "Payment failed: #{error.message}")
|> render(:new, changeset: Payment.changeset(%Payment{}, payment_params))
end
end
def thank_you(conn, %{"amount" => amount}) do
render(conn, :thank_you, amount: amount)
end
end
6. HTML Components
Create lib/your_app_web/controllers/payment_html.ex:
defmodule YourAppWeb.PaymentHTML do
use YourAppWeb, :html
embed_templates "payment_html/*"
attr :changeset, Ecto.Changeset, required: true
attr :action, :string, required: true
def payment_form(assigns) do
~H"""
<div class="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-md">
<.form :let={f} for={@changeset} action={@action} class="space-y-6">
<div>
<.input field={f[:amount]} type="number" label="Amount (in cents)" class="mt-1 block w-full" />
</div>
<div>
<label for="card-element" class="block text-sm font-medium text-gray-700">Credit or debit card</label>
<div id="card-element" class="mt-1 block w-full">
<!-- Stripe Elements will insert the card input here -->
</div>
<div id="card-errors" role="alert" class="mt-2 text-sm text-red-600"></div>
</div>
<input type="hidden" name={input_name(f, :token)} id="stripe_token" value={input_value(f, :token)} />
<div>
<.button type="submit" class="w-full">Pay Now</.button>
</div>
</.form>
</div>
"""
end
attr :amount, :string, required: true
def thank_you(assigns)
end
7. Templates
Create lib/your_app_web/controllers/payment_html/new.html.heex:
<h1 class="text-3xl font-bold text-center mt-8 mb-6">New Payment</h1>
<.payment_form changeset={@changeset} action={~p"/payment"} />
<script src="https://js.stripe.com/v3/"></script>
<script>
var stripe = Stripe('<%= @stripe_publishable_key %>');
var elements = stripe.elements();
var style = {
base: {
fontSize: '16px',
color: '#32325d',
'::placeholder': {
color: '#aab7c4'
},
},
invalid: {
color: '#fa755a',
iconColor: '#fa755a'
}
};
var card = elements.create('card', {style: style});
card.mount('#card-element');
card.addEventListener('change', function(event) {
var displayError = document.getElementById('card-errors');
if (event.error) {
displayError.textContent = event.error.message;
} else {
displayError.textContent = '';
}
});
var form = document.querySelector('form');
form.addEventListener('submit', function(event) {
event.preventDefault();
stripe.createToken(card).then(function(result) {
if (result.error) {
var errorElement = document.getElementById('card-errors');
errorElement.textContent = result.error.message;
} else {
var tokenInput = document.getElementById('stripe_token');
tokenInput.value = result.token.id;
form.submit();
}
});
});
</script>
Create lib/your_app_web/controllers/payment_html/thank_you.html.heex:
<div class="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-md text-center">
<h1 class="text-3xl font-bold mb-4">Thank You!</h1>
<p class="text-xl mb-4">Your payment of $<%= String.to_float(@amount) / 100 %> was successful.</p>
<p class="mb-6">We appreciate your business and hope you enjoy your purchase.</p>
<.link href={~p"/"} class="inline-block bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Return to Home
</.link>
</div>
8. Wire up the router
In lib/your_app_web/router.ex:
scope "/", YourAppWeb do
pipe_through :browser
# existing routes
get "/payment/new", PaymentController, :new
post "/payment", PaymentController, :create
get "/payment/thank-you", PaymentController, :thank_you
end
Stripity Stripe Integration
The create action in the PaymentController demonstrates the simplicity of using Stripity Stripe. The Stripe.Charge.create/1 function encapsulates the complexities of creating a charge, including constructing headers, encoding the request body, and parsing the response.
Conclusion
This guide demonstrates the basic implementation of Stripe payments in a Phoenix 1.7 application using Stripity Stripe. The new component-based structure in Phoenix 1.7 promotes modular code organization, although it requires some adjustment in development approach.
This implementation covers the fundamentals of payment processing. For more advanced features such as subscriptions or invoicing, refer to the Stripity Stripe documentation.
03 Sep 2024
Zig’s Print Function: Why the Empty Struct?
If you’ve used Zig, you’ve likely encountered this syntax:
std.debug.print("Hello, world!\n", .{});
The empty struct (.{}) at the end might seem puzzling, especially when printing a static string. This post explores the rationale behind this design choice and its implications for type safety and security.
The Printf Legacy and Its Drawbacks
C’s printf function has long been a standard for formatted output in many languages. However, it comes with significant drawbacks in type safety and security. Zig’s approach addresses these issues, albeit with a syntax that may initially seem counterintuitive.
Let’s examine the problems with printf-style functions and how Zig’s solution, while more verbose, provides important safeguards.
The Problem with Printf
C’s printf function and its variants are variadic, meaning they can take a variable number of arguments. While this provides flexibility, it also opens the door to several potential issues.
1. Type Safety Concerns
The printf function relies on format specifiers in the format string to determine how to interpret and print its arguments. However, the compiler can’t always verify if the format specifiers match the types of the provided arguments. This can lead to various problems:
#include <stdio.h>
int main() {
// Example 1: Type mismatch
int num = 42;
printf("The number is %f\n", num); // Should be %d for int
// Example 2: Wrong number of arguments
printf("Name: %s, Age: %d\n", "Alice"); // Missing the age argument
// Example 3: Passing wrong type of pointer
char* str = "Hello";
printf("String: %s\n", &str); // Should be just str, not &str
return 0;
}
In these examples:
- We’re passing an integer to a float format specifier.
- We’re missing an argument for the age.
- We’re passing a pointer to a string pointer instead of just the string pointer.
All of these will compile with warnings (if warnings are enabled), but they can lead to undefined behavior at runtime. The program might crash, print garbage values, or in some cases, even appear to work correctly (which can be dangerous as it might go unnoticed).
2. Security Vulnerabilities
Even more concerning are the potential security vulnerabilities, particularly format string attacks. These occur when an attacker can control the format string passed to a printf-style function. Here’s a vulnerable example:
#include <stdio.h>
void vulnerable_function(char *user_input) {
printf(user_input);
}
int main() {
char user_input[100];
printf("Enter a string: ");
fgets(user_input, sizeof(user_input), stdin);
vulnerable_function(user_input);
return 0;
}
In this code, vulnerable_function directly passes user_input as the format string to printf. An attacker could exploit this by entering format specifiers as input:
%x %x %x %x could print values from the stack, potentially leaking sensitive information.
%n could be used to write to memory locations, potentially overwriting important data or hijacking program flow.
Modern Solutions: The Zig Approach
Modern languages like Zig have recognized these issues and taken steps to address them. Let’s look at how Zig handles string formatting:
const std = @import("std");
pub fn main() !void {
const number = 42;
std.debug.print("The number is {}\n", .{number});
}
Key differences in Zig’s approach:
-
Type Safety: The compiler always knows the types of the arguments being passed, ensuring type safety at compile-time.
-
Format String Separation: The format string and the arguments are separate parameters. The .{number} syntax creates an anonymous struct of arguments.
-
Compile-time Checking: Zig performs compile-time checking to ensure the format string matches the provided arguments.
-
No Variadic Functions: By not using variadic functions, Zig eliminates an entire class of potential issues.
While this approach may seem more verbose for simple cases (e.g., std.debug.print("Hello, world!\n", .{});), it provides significant safety benefits:
- Impossible to mismatch format specifiers and argument types
- No risk of format string attacks
- Clear separation between the format string and the data being formatted
Zig’s Design Philosophy
Zig’s approach to string formatting isn’t just a technical solution; it’s a reflection of the language’s broader design philosophy. One of Zig’s zen tenets particularly relevant to this discussion is:
“Compile errors are better than runtime crashes.”
This principle is clearly illustrated in Zig’s handling of string formatting:
-
Type Checking at Compile Time: By separating the format string and the arguments, and using compile-time type checking, Zig ensures that type mismatches are caught before the program ever runs.
-
No Format String Vulnerabilities: The design makes it impossible to introduce format string vulnerabilities, eliminating an entire class of runtime security issues at compile time.
-
Explicit Argument Passing: The .{} syntax for passing arguments is more explicit, reducing the chance of accidentally omitting arguments or passing the wrong number of arguments.
By prioritizing compile-time checks and explicit syntax, Zig embodies its philosophy of preferring compile errors to runtime crashes. This not only improves program safety but also enhances the developer experience by catching potential issues early in the development process.
Conclusion
The design choices in Zig’s string formatting system, guided by principles like “Compile errors are better than runtime crashes,” demonstrate how language design can significantly impact code safety, readability, and maintainability. As we’ve seen, these choices can eliminate entire categories of bugs and vulnerabilities, making a strong case for prioritizing safety and explicitness in language design.