Skip to content

Commit

Permalink
Merge branch 'main' into cm-phx-new-cache-dir
Browse files Browse the repository at this point in the history
  • Loading branch information
chrismccord authored Feb 20, 2025
2 parents aa6c7eb + 2a345fd commit e59d101
Show file tree
Hide file tree
Showing 132 changed files with 3,349 additions and 3,524 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
matrix:
include:
- elixir: 1.15.8
otp: 24.3.4.17
otp: 25.3.2.9

- elixir: 1.17.3
otp: 27.2
Expand Down Expand Up @@ -60,6 +60,7 @@ jobs:
- name: Run installer test
run: |
cd installer
mix deps.get
mix test
if: ${{ matrix.installer }}

Expand Down Expand Up @@ -107,7 +108,7 @@ jobs:
include:
# look for correct alpine image here: https://hub.docker.com/r/hexpm/elixir/tags
- elixir: 1.15.8
otp: 24.3.4.17
otp: 25.3.2.9
suffix: "alpine-3.20.3"

- elixir: 1.17.3
Expand All @@ -118,6 +119,7 @@ jobs:
image: hexpm/elixir:${{ matrix.elixir }}-erlang-${{ matrix.otp }}-${{ matrix.suffix }}
env:
ELIXIR_ASSERT_TIMEOUT: 10000
PHX_CI: true
services:
postgres:
image: postgres
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Changelog for v1.8

This release requires Erlang/OTP 25+.

## v1.7

The CHANGELOG for v1.7 releases can be found in the [v1.7 branch](https://github.com/phoenixframework/phoenix/blob/v1.7/CHANGELOG.md).
10 changes: 6 additions & 4 deletions assets/js/phoenix/ajax.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import {

export default class Ajax {

static request(method, endPoint, accept, body, timeout, ontimeout, callback){
static request(method, endPoint, headers, body, timeout, ontimeout, callback){
if(global.XDomainRequest){
let req = new global.XDomainRequest() // IE8, IE9
return this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback)
} else {
let req = new global.XMLHttpRequest() // IE7+, Firefox, Chrome, Opera, Safari
return this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback)
return this.xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback)
}
}

Expand All @@ -31,10 +31,12 @@ export default class Ajax {
return req
}

static xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback){
static xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback){
req.open(method, endPoint, true)
req.timeout = timeout
req.setRequestHeader("Content-Type", accept)
for (let [key, value] of Object.entries(headers)) {
req.setRequestHeader(key, value)
}
req.onerror = () => callback && callback(null)
req.onreadystatechange = () => {
if(req.readyState === XHR_STATES.complete && callback){
Expand Down
3 changes: 2 additions & 1 deletion assets/js/phoenix/constants.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const globalSelf = typeof self !== "undefined" ? self : null
export const phxWindow = typeof window !== "undefined" ? window : null
export const global = globalSelf || phxWindow || global
export const global = globalSelf || phxWindow || globalThis
export const DEFAULT_VSN = "2.0.0"
export const SOCKET_STATES = {connecting: 0, open: 1, closing: 2, closed: 3}
export const DEFAULT_TIMEOUT = 10000
Expand All @@ -27,3 +27,4 @@ export const TRANSPORTS = {
export const XHR_STATES = {
complete: 4
}
export const AUTH_TOKEN_PREFIX = "base64url.bearer.phx."
20 changes: 15 additions & 5 deletions assets/js/phoenix/longpoll.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
SOCKET_STATES,
TRANSPORTS
TRANSPORTS,
AUTH_TOKEN_PREFIX
} from "./constants"

import Ajax from "./ajax"
Expand All @@ -15,7 +16,12 @@ let arrayBufferToBase64 = (buffer) => {

export default class LongPoll {

constructor(endPoint){
constructor(endPoint, protocols){
// we only support subprotocols for authToken
// ["phoenix", "base64url.bearer.phx.BASE64_ENCODED_TOKEN"]
if (protocols.length === 2 && protocols[1].startsWith(AUTH_TOKEN_PREFIX)) {
this.authToken = atob(protocols[1].slice(AUTH_TOKEN_PREFIX.length))
}
this.endPoint = null
this.token = null
this.skipHeartbeat = true
Expand Down Expand Up @@ -58,7 +64,11 @@ export default class LongPoll {
isActive(){ return this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting }

poll(){
this.ajax("GET", "application/json", null, () => this.ontimeout(), resp => {
const headers = {"Accept": "application/json"}
if(this.authToken){
headers["X-Phoenix-AuthToken"] = this.authToken
}
this.ajax("GET", headers, null, () => this.ontimeout(), resp => {
if(resp){
var {status, token, messages} = resp
this.token = token
Expand Down Expand Up @@ -160,13 +170,13 @@ export default class LongPoll {
}
}

ajax(method, contentType, body, onCallerTimeout, callback){
ajax(method, headers, body, onCallerTimeout, callback){
let req
let ontimeout = () => {
this.reqs.delete(req)
onCallerTimeout()
}
req = Ajax.request(method, this.endpointURL(), contentType, body, this.timeout, ontimeout, resp => {
req = Ajax.request(method, this.endpointURL(), headers, body, this.timeout, ontimeout, resp => {
this.reqs.delete(req)
if(this.isActive()){ callback(resp) }
})
Expand Down
14 changes: 12 additions & 2 deletions assets/js/phoenix/socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
DEFAULT_VSN,
SOCKET_STATES,
TRANSPORTS,
WS_CLOSE_NORMAL
WS_CLOSE_NORMAL,
AUTH_TOKEN_PREFIX
} from "./constants"

import {
Expand Down Expand Up @@ -86,6 +87,8 @@ import Timer from "./timer"
* Defaults to 20s (double the server long poll timer).
*
* @param {(Object|function)} [opts.params] - The optional params to pass when connecting
* @param {string} [opts.authToken] - the optional authentication token to be exposed on the server
* under the `:auth_token` connect_info key.
* @param {string} [opts.binaryType] - The binary type to use for binary WebSocket frames.
*
* Defaults to "arraybuffer"
Expand Down Expand Up @@ -176,6 +179,7 @@ export default class Socket {
this.reconnectTimer = new Timer(() => {
this.teardown(() => this.connect())
}, this.reconnectAfterMs)
this.authToken = opts.authToken
}

/**
Expand Down Expand Up @@ -345,7 +349,13 @@ export default class Socket {
transportConnect(){
this.connectClock++
this.closeWasClean = false
this.conn = new this.transport(this.endPointURL())
let protocols = ["phoenix"]
// Sec-WebSocket-Protocol based token
// (longpoll uses Authorization header instead)
if (this.authToken) {
protocols.push(`${AUTH_TOKEN_PREFIX}${btoa(this.authToken).replace(/=/g, "")}`)
}
this.conn = new this.transport(this.endPointURL(), protocols)
this.conn.binaryType = this.binaryType
this.conn.timeout = this.longpollerTimeout
this.conn.onopen = () => this.onConnOpen()
Expand Down
13 changes: 12 additions & 1 deletion assets/test/channel_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ const defaultRef = 1
const defaultTimeout = 10000

class WSMock {
constructor(){}
constructor(url, protocols){
this.url = url
this.protocols = protocols
}
close(){}
send(){}
}
Expand Down Expand Up @@ -58,6 +61,14 @@ describe("with transport", function (){
expect(joinPush.event).toBe("phx_join")
expect(joinPush.timeout).toBe(1234)
})

it("sets subprotocols when authToken is provided", function (){
const authToken = "1234"
const socket = new Socket("/socket", {authToken})

socket.connect()
expect(socket.conn.protocols).toEqual(["phoenix", "base64url.bearer.phx.MTIzNA"])
})
})

describe("updating join params", function (){
Expand Down
3 changes: 2 additions & 1 deletion assets/test/socket_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,8 @@ describe("with transports", function (){
}

const result = socket.ping(rtt => {
expect(rtt >= latency).toBe(true)
// if we're unlucky we could also receive 99 as rtt, so let's be generous
expect(rtt >= (latency - 10)).toBe(true)
done()
})
expect(result).toBe(true)
Expand Down
12 changes: 6 additions & 6 deletions guides/asset_management.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ If you want to import JavaScript dependencies, you have at least three options t

1. Vendor those dependencies inside your project and import them in your "assets/js/app.js" using a relative path:

```js
```javascript
import topbar from "../vendor/topbar"
```

2. Call `npm install topbar --prefix assets` will create `package.json` and `package-lock.json` inside your assets directory and `esbuild` will be able to automatically pick them up:

```js
```javascript
import topbar from "topbar"
```

Expand All @@ -35,7 +35,7 @@ If you want to import JavaScript dependencies, you have at least three options t

Run `mix deps.get` to fetch the dependency and then import it:

```js
```javascript
import topbar from "topbar"
```

Expand Down Expand Up @@ -67,7 +67,7 @@ error: Could not resolve "/images/bg.png" (mark it as external to exclude it fro
Given the images are already managed by Phoenix, you need to mark all resources from `/images` (and also `/fonts`) as external, as the error message says. This is what Phoenix does by default for new apps since v1.6.1+. In your `config/config.exs`, you will find:

```elixir
args: ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
args: ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
```

If you need to reference other directories, you need to update the arguments above accordingly. Note running `mix phx.digest` will create digested files for all of the assets in `priv/static`, so your images and fonts are still cache-busted.
Expand All @@ -94,7 +94,7 @@ $ yarn add ../deps/phoenix ../deps/phoenix_html ../deps/phoenix_live_view

Next, add a custom JavaScript build script. We'll call the example `assets/build.js`:

```js
```javascript
const esbuild = require("esbuild");

const args = process.argv.slice(2);
Expand All @@ -114,7 +114,7 @@ let opts = {
entryPoints: ["js/app.js"],
bundle: true,
logLevel: "info",
target: "es2017",
target: "es2022",
outdir: "../priv/static/assets",
external: ["*.css", "fonts/*", "images/*"],
nodePaths: ["../deps"],
Expand Down
15 changes: 10 additions & 5 deletions guides/authentication/mix_phx_gen_auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> This guide assumes that you have gone through the [introductory guides](overview.html) and have a Phoenix application [up and running](up_and_running.html).
The `mix phx.gen.auth` command generates a flexible, pre-built authentication system into your Phoenix app. This generator allows you to quickly move past the task of adding authentication to your codebase and stay focused on the real-world problem your application is trying to solve.
The `mix phx.gen.auth` command generates a flexible, pre-built authentication system based on magic links into your Phoenix app. This generator allows you to quickly move past the task of adding authentication to your codebase and stay focused on the real-world problem your application is trying to solve.

## Getting started

Expand Down Expand Up @@ -76,11 +76,10 @@ The generated code ships with an authentication module with a handful of plugs t

* `fetch_current_user` - fetches the current user information if available
* `require_authenticated_user` - must be invoked after `fetch_current_user` and requires that a current user exists and is authenticated
* `redirect_if_user_is_authenticated` - used for the few pages that must not be available to authenticated users
* `redirect_if_user_is_authenticated` - used for the few pages that must not be available to authenticated users (only generated for controller based authentication)
* `require_sudo_mode` - used for pages that contain sensitive operations and enforces recent authentication

### Confirmation

The generated functionality ships with an account confirmation mechanism, where users have to confirm their account, typically by email. However, the generated code does not forbid users from using the application if their accounts have not yet been confirmed. You can add this functionality by customizing the `require_authenticated_user` in the `Auth` module to check for the `confirmed_at` field (and any other property you desire).
There are similar `:on_mount` hooks for LiveView based authentication.

### Notifiers

Expand All @@ -102,6 +101,10 @@ If your application is sensitive to enumeration attacks, you need to implement y

Furthermore, if you are concerned about enumeration attacks, beware of timing attacks too. For example, registering a new account typically involves additional work (such as writing to the database, sending emails, etc) compared to when an account already exists. Someone could measure the time taken to execute those additional tasks to enumerate emails. This applies to all endpoints (registration, confirmation, password recovery, etc.) that may send email, in-app notifications, etc.

### Confirmation and credential pre-stuffing attacks

The generated functionality ships with an account confirmation mechanism, where users have to confirm their account, typically by email. Furthermore, to prevent security issues, the generated code does forbid users from using the application if their accounts have not yet been confirmed. If you want to change this behavior, please refer to the ["Mixing magic link and password registration" section](Mix.Tasks.Phx.Gen.Auth.html#module-mixing-magic-link-and-password-registration) of `mix phx.gen.auth`.

### Case sensitiveness

The email lookup is made to be case-insensitive. Case-insensitive lookups are the default in MySQL and MSSQL. In SQLite3 we use [`COLLATE NOCASE`](https://www.sqlite.org/datatype3.html#collating_sequences) in the column definition to support it. In PostgreSQL, we use the [`citext` extension](https://www.postgresql.org/docs/current/citext.html).
Expand All @@ -125,6 +128,8 @@ The following links have more information regarding the motivation and design of
* The [original `phx_gen_auth` repo][phx_gen_auth repo] (for Phoenix 1.5 applications) - This is a great resource to see discussions around decisions that have been made in earlier versions of the project.
* [Original pull request on bare Phoenix app][auth PR]
* [Original design spec](https://github.com/dashbitco/mix_phx_gen_auth_demo/blob/auth/README.md)
* [Pull request for migrating LiveView based Phoenix 1.7 `phx.gen.auth` to magic links](https://github.com/SteffenDE/phoenix_gen_auth_magic_link/pull/1)
* [Pull request for migrating controller based Phoenix 1.7 `phx.gen.auth` to magic links](https://github.com/SteffenDE/phoenix_gen_auth_magic_link/pull/2)

[phx_gen_auth repo]: https://github.com/aaronrenner/phx_gen_auth
[auth PR]: https://github.com/dashbitco/mix_phx_gen_auth_demo/pull/1
8 changes: 7 additions & 1 deletion guides/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,20 @@ What it doesn't have is a view for rendering JSON. Phoenix Controller hands off
quote do
use Phoenix.Controller,
formats: [:html, :json],
layouts: [html: HelloWeb.Layouts]
layouts: [html: {HelloWeb.Layouts, :app}]
...
end
end
```

So out of the box Phoenix will look for a `HTML` and `JSON` view modules based on the request format and the controller name. We can also explicitly tell Phoenix in our controller which view(s) to use for each format. For example, what Phoenix does by default can be explicitly set with the following in your controller:

```elixir
plug :put_view, html: {HelloWeb.PageHTML, :app}, json: {HelloWeb.PageJSON, :app}
```

The layout name can be omitted, in which case the default layout name `:app` is used, so the above is equivalent to:

```elixir
plug :put_view, html: HelloWeb.PageHTML, json: HelloWeb.PageJSON
```
Expand Down
4 changes: 4 additions & 0 deletions guides/introduction/community.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ The Security Working Group of the Erlang Ecosystem Foundation also publishes in-

## Screencasts/Courses

* [Free Bootcamp: Fullstack Elixir and Phoenix (by TechSchool - 2024)](https://techschool.dev/en/bootcamps/fullstack-elixir-and-phoenix)

* [Learn Phoenix LiveView (by George Arrowsmith - 2024)](https://phoenixliveview.com)

* [Phoenix LiveView Free Course (by The Pragmatic Studio - 2023)](https://pragmaticstudio.com/courses/phoenix-liveview)

* [Build It With Phoenix video course (by Geoffrey Lessel - 2023)](https://builditwithphoenix.com)
Expand Down
Loading

0 comments on commit e59d101

Please sign in to comment.