Skip to content

Commit e59d101

Browse files
authored
Merge branch 'main' into cm-phx-new-cache-dir
2 parents aa6c7eb + 2a345fd commit e59d101

File tree

132 files changed

+3349
-3524
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

132 files changed

+3349
-3524
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
matrix:
1616
include:
1717
- elixir: 1.15.8
18-
otp: 24.3.4.17
18+
otp: 25.3.2.9
1919

2020
- elixir: 1.17.3
2121
otp: 27.2
@@ -60,6 +60,7 @@ jobs:
6060
- name: Run installer test
6161
run: |
6262
cd installer
63+
mix deps.get
6364
mix test
6465
if: ${{ matrix.installer }}
6566

@@ -107,7 +108,7 @@ jobs:
107108
include:
108109
# look for correct alpine image here: https://hub.docker.com/r/hexpm/elixir/tags
109110
- elixir: 1.15.8
110-
otp: 24.3.4.17
111+
otp: 25.3.2.9
111112
suffix: "alpine-3.20.3"
112113

113114
- elixir: 1.17.3
@@ -118,6 +119,7 @@ jobs:
118119
image: hexpm/elixir:${{ matrix.elixir }}-erlang-${{ matrix.otp }}-${{ matrix.suffix }}
119120
env:
120121
ELIXIR_ASSERT_TIMEOUT: 10000
122+
PHX_CI: true
121123
services:
122124
postgres:
123125
image: postgres

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Changelog for v1.8
22

3+
This release requires Erlang/OTP 25+.
4+
35
## v1.7
46

57
The CHANGELOG for v1.7 releases can be found in the [v1.7 branch](https://github.com/phoenixframework/phoenix/blob/v1.7/CHANGELOG.md).

assets/js/phoenix/ajax.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import {
55

66
export default class Ajax {
77

8-
static request(method, endPoint, accept, body, timeout, ontimeout, callback){
8+
static request(method, endPoint, headers, body, timeout, ontimeout, callback){
99
if(global.XDomainRequest){
1010
let req = new global.XDomainRequest() // IE8, IE9
1111
return this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback)
1212
} else {
1313
let req = new global.XMLHttpRequest() // IE7+, Firefox, Chrome, Opera, Safari
14-
return this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback)
14+
return this.xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback)
1515
}
1616
}
1717

@@ -31,10 +31,12 @@ export default class Ajax {
3131
return req
3232
}
3333

34-
static xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback){
34+
static xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback){
3535
req.open(method, endPoint, true)
3636
req.timeout = timeout
37-
req.setRequestHeader("Content-Type", accept)
37+
for (let [key, value] of Object.entries(headers)) {
38+
req.setRequestHeader(key, value)
39+
}
3840
req.onerror = () => callback && callback(null)
3941
req.onreadystatechange = () => {
4042
if(req.readyState === XHR_STATES.complete && callback){

assets/js/phoenix/constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export const globalSelf = typeof self !== "undefined" ? self : null
22
export const phxWindow = typeof window !== "undefined" ? window : null
3-
export const global = globalSelf || phxWindow || global
3+
export const global = globalSelf || phxWindow || globalThis
44
export const DEFAULT_VSN = "2.0.0"
55
export const SOCKET_STATES = {connecting: 0, open: 1, closing: 2, closed: 3}
66
export const DEFAULT_TIMEOUT = 10000
@@ -27,3 +27,4 @@ export const TRANSPORTS = {
2727
export const XHR_STATES = {
2828
complete: 4
2929
}
30+
export const AUTH_TOKEN_PREFIX = "base64url.bearer.phx."

assets/js/phoenix/longpoll.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
SOCKET_STATES,
3-
TRANSPORTS
3+
TRANSPORTS,
4+
AUTH_TOKEN_PREFIX
45
} from "./constants"
56

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

1617
export default class LongPoll {
1718

18-
constructor(endPoint){
19+
constructor(endPoint, protocols){
20+
// we only support subprotocols for authToken
21+
// ["phoenix", "base64url.bearer.phx.BASE64_ENCODED_TOKEN"]
22+
if (protocols.length === 2 && protocols[1].startsWith(AUTH_TOKEN_PREFIX)) {
23+
this.authToken = atob(protocols[1].slice(AUTH_TOKEN_PREFIX.length))
24+
}
1925
this.endPoint = null
2026
this.token = null
2127
this.skipHeartbeat = true
@@ -58,7 +64,11 @@ export default class LongPoll {
5864
isActive(){ return this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting }
5965

6066
poll(){
61-
this.ajax("GET", "application/json", null, () => this.ontimeout(), resp => {
67+
const headers = {"Accept": "application/json"}
68+
if(this.authToken){
69+
headers["X-Phoenix-AuthToken"] = this.authToken
70+
}
71+
this.ajax("GET", headers, null, () => this.ontimeout(), resp => {
6272
if(resp){
6373
var {status, token, messages} = resp
6474
this.token = token
@@ -160,13 +170,13 @@ export default class LongPoll {
160170
}
161171
}
162172

163-
ajax(method, contentType, body, onCallerTimeout, callback){
173+
ajax(method, headers, body, onCallerTimeout, callback){
164174
let req
165175
let ontimeout = () => {
166176
this.reqs.delete(req)
167177
onCallerTimeout()
168178
}
169-
req = Ajax.request(method, this.endpointURL(), contentType, body, this.timeout, ontimeout, resp => {
179+
req = Ajax.request(method, this.endpointURL(), headers, body, this.timeout, ontimeout, resp => {
170180
this.reqs.delete(req)
171181
if(this.isActive()){ callback(resp) }
172182
})

assets/js/phoenix/socket.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
DEFAULT_VSN,
77
SOCKET_STATES,
88
TRANSPORTS,
9-
WS_CLOSE_NORMAL
9+
WS_CLOSE_NORMAL,
10+
AUTH_TOKEN_PREFIX
1011
} from "./constants"
1112

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

181185
/**
@@ -345,7 +349,13 @@ export default class Socket {
345349
transportConnect(){
346350
this.connectClock++
347351
this.closeWasClean = false
348-
this.conn = new this.transport(this.endPointURL())
352+
let protocols = ["phoenix"]
353+
// Sec-WebSocket-Protocol based token
354+
// (longpoll uses Authorization header instead)
355+
if (this.authToken) {
356+
protocols.push(`${AUTH_TOKEN_PREFIX}${btoa(this.authToken).replace(/=/g, "")}`)
357+
}
358+
this.conn = new this.transport(this.endPointURL(), protocols)
349359
this.conn.binaryType = this.binaryType
350360
this.conn.timeout = this.longpollerTimeout
351361
this.conn.onopen = () => this.onConnOpen()

assets/test/channel_test.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ const defaultRef = 1
77
const defaultTimeout = 10000
88

99
class WSMock {
10-
constructor(){}
10+
constructor(url, protocols){
11+
this.url = url
12+
this.protocols = protocols
13+
}
1114
close(){}
1215
send(){}
1316
}
@@ -58,6 +61,14 @@ describe("with transport", function (){
5861
expect(joinPush.event).toBe("phx_join")
5962
expect(joinPush.timeout).toBe(1234)
6063
})
64+
65+
it("sets subprotocols when authToken is provided", function (){
66+
const authToken = "1234"
67+
const socket = new Socket("/socket", {authToken})
68+
69+
socket.connect()
70+
expect(socket.conn.protocols).toEqual(["phoenix", "base64url.bearer.phx.MTIzNA"])
71+
})
6172
})
6273

6374
describe("updating join params", function (){

assets/test/socket_test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,8 @@ describe("with transports", function (){
843843
}
844844

845845
const result = socket.ping(rtt => {
846-
expect(rtt >= latency).toBe(true)
846+
// if we're unlucky we could also receive 99 as rtt, so let's be generous
847+
expect(rtt >= (latency - 10)).toBe(true)
847848
done()
848849
})
849850
expect(result).toBe(true)

guides/asset_management.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ If you want to import JavaScript dependencies, you have at least three options t
1616

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

19-
```js
19+
```javascript
2020
import topbar from "../vendor/topbar"
2121
```
2222

2323
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:
2424

25-
```js
25+
```javascript
2626
import topbar from "topbar"
2727
```
2828

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

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

38-
```js
38+
```javascript
3939
import topbar from "topbar"
4040
```
4141

@@ -67,7 +67,7 @@ error: Could not resolve "/images/bg.png" (mark it as external to exclude it fro
6767
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:
6868

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

7373
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.
@@ -94,7 +94,7 @@ $ yarn add ../deps/phoenix ../deps/phoenix_html ../deps/phoenix_live_view
9494

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

97-
```js
97+
```javascript
9898
const esbuild = require("esbuild");
9999

100100
const args = process.argv.slice(2);
@@ -114,7 +114,7 @@ let opts = {
114114
entryPoints: ["js/app.js"],
115115
bundle: true,
116116
logLevel: "info",
117-
target: "es2017",
117+
target: "es2022",
118118
outdir: "../priv/static/assets",
119119
external: ["*.css", "fonts/*", "images/*"],
120120
nodePaths: ["../deps"],

guides/authentication/mix_phx_gen_auth.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> 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).
44
5-
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.
5+
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.
66

77
## Getting started
88

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

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

81-
### Confirmation
82-
83-
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).
82+
There are similar `:on_mount` hooks for LiveView based authentication.
8483

8584
### Notifiers
8685

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

103102
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.
104103

104+
### Confirmation and credential pre-stuffing attacks
105+
106+
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`.
107+
105108
### Case sensitiveness
106109

107110
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).
@@ -125,6 +128,8 @@ The following links have more information regarding the motivation and design of
125128
* 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.
126129
* [Original pull request on bare Phoenix app][auth PR]
127130
* [Original design spec](https://github.com/dashbitco/mix_phx_gen_auth_demo/blob/auth/README.md)
131+
* [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)
132+
* [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)
128133

129134
[phx_gen_auth repo]: https://github.com/aaronrenner/phx_gen_auth
130135
[auth PR]: https://github.com/dashbitco/mix_phx_gen_auth_demo/pull/1

guides/controllers.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,14 +182,20 @@ What it doesn't have is a view for rendering JSON. Phoenix Controller hands off
182182
quote do
183183
use Phoenix.Controller,
184184
formats: [:html, :json],
185-
layouts: [html: HelloWeb.Layouts]
185+
layouts: [html: {HelloWeb.Layouts, :app}]
186186
...
187187
end
188188
end
189189
```
190190

191191
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:
192192

193+
```elixir
194+
plug :put_view, html: {HelloWeb.PageHTML, :app}, json: {HelloWeb.PageJSON, :app}
195+
```
196+
197+
The layout name can be omitted, in which case the default layout name `:app` is used, so the above is equivalent to:
198+
193199
```elixir
194200
plug :put_view, html: HelloWeb.PageHTML, json: HelloWeb.PageJSON
195201
```

guides/introduction/community.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ The Security Working Group of the Erlang Ecosystem Foundation also publishes in-
3434

3535
## Screencasts/Courses
3636

37+
* [Free Bootcamp: Fullstack Elixir and Phoenix (by TechSchool - 2024)](https://techschool.dev/en/bootcamps/fullstack-elixir-and-phoenix)
38+
39+
* [Learn Phoenix LiveView (by George Arrowsmith - 2024)](https://phoenixliveview.com)
40+
3741
* [Phoenix LiveView Free Course (by The Pragmatic Studio - 2023)](https://pragmaticstudio.com/courses/phoenix-liveview)
3842

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

0 commit comments

Comments
 (0)