Skip to content

Commit a76587d

Browse files
committedAug 26, 2023
Updated docs + new websockets guide
1 parent ffd0f6f commit a76587d

6 files changed

+318
-69
lines changed
 

‎README.md

+14-44
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ This is a comprehensively updated fork of [Sebastián Ramírez's](https://github
1515
- [Development and installation](./docs/development-guide.md)
1616
- [Deployment for production](./docs/deployment-guide.md)
1717
- [Authentication and magic tokens](./docs/authentication-guide.md)
18+
- [Websockets for interactive communication](./docs/websocket-guide.md)
1819
- [More details](#more-details)
1920
- [Help needed](#help-needed)
2021
- [Release notes](#release-notes)
@@ -78,6 +79,7 @@ This FastAPI, PostgreSQL, Neo4j & Nuxt 3 repo will generate a complete web appli
7879
- [Development and installation](./docs/development-guide.md)
7980
- [Deployment for production](./docs/deployment-guide.md)
8081
- [Authentication and magic tokens](./docs/authentication-guide.md)
82+
- [Websockets for interactive communication](./docs/websocket-guide.md)
8183

8284
## More details
8385

@@ -99,6 +101,18 @@ The tests are broken and it would be great if someone could take that on. Other
99101

100102
## Release Notes
101103

104+
See notes and [releases](https://github.com/whythawk/full-stack-fastapi-postgresql/releases).
105+
106+
## 0.8.2
107+
108+
Fixing [#39](https://github.com/whythawk/full-stack-fastapi-postgresql/issues/39), thanks to @a-vorobyoff:
109+
110+
- Exposing port 24678 for Vite on frontend in development mode.
111+
- Ensuring Nuxt content on /api/_content doesn't interfere with backend /api/v routes.
112+
- Checking for password before hashing on user creation.
113+
- Updating generated README for Hatch (after Poetry deprecation).
114+
- Minor fixes.
115+
102116
### 0.8.1
103117

104118
- Minor updates to Docker scripts for `build`.
@@ -124,50 +138,6 @@ The tests are broken and it would be great if someone could take that on. Other
124138
- Fixed: Updated token url in deps.py [#29](https://github.com/whythawk/full-stack-fastapi-postgresql/pull/29) by @vusa
125139
- Docs: Reorganised documentation [#21](https://github.com/whythawk/full-stack-fastapi-postgresql/pull/21) by @turukawa
126140

127-
### 0.7.3
128-
- @nuxt/content 2.2.1 -> 2.4.3
129-
- Fixed: `@nuxt/content` default api, `/api/_content`, conflicts with the `backend` api url preventing content pages loading.
130-
- Documentation: Complete deployment guide in `DEPLOYMENT-README.md` (this has now been moved to `/docs`)
131-
132-
### 0.7.2
133-
- Fixed: URLs for recreating project in generated `README.md`. PR [#15](https://github.com/whythawk/full-stack-fastapi-postgresql/pull/15) by @FranzForstmayr
134-
- Fixed: Absolute path for mount point in `docker-compose.override.yml`. PR [#16](https://github.com/whythawk/full-stack-fastapi-postgresql/pull/16) by @FranzForstmayr
135-
- Fixed: Login artifacts left over from before switch to magic auth. PR [#18](https://github.com/whythawk/full-stack-fastapi-postgresql/pull/18) by @turukawa and @FranzForstmayr
136-
- New: New floating magic login card. PR [#19](https://github.com/whythawk/full-stack-fastapi-postgresql/pull/19) by @turukawa
137-
- New: New site contact page. PR [#20](https://github.com/whythawk/full-stack-fastapi-postgresql/pull/20) by @turukawa
138-
139-
### 0.7.1
140-
141-
- SQLAlchemy 1.4 -> 2.0
142-
- Nuxt.js 3.0 -> 3.2.2
143-
- Fixed: `tokenUrl` in `app/api/deps.py`. Thanks to @Choiuijin1125.
144-
- Fixed: SMTP options for TLS must be `ssl`. Thanks to @raouldo.
145-
- Fixed: `libgeos` is a dependency for `shapely` which is a dependency for `neomodel`, and which doesn't appear to be installed correctly on Macs. Thanks to @valsha and @Mocha-L.
146-
- Fixed: `frontend` fails to start in development. Thanks to @pabloapast and @dividor.
147-
148-
### 0.7.0
149-
150-
- New feature: magic (email-based) login, with password fallback
151-
- New feature: Time-based One-Time Password (TOTP) authentication
152-
- Security enhancements to improve consistency, safety and reliability of the authentication process (see full description in the frontend app)
153-
- Requires one new `frontend` dependency: [QRcode.vue](https://github.com/scopewu/qrcode.vue)
154-
155-
### 0.6.1
156-
157-
- Corrected error in variable name `ACCESS_TOKEN_EXPIRE_SECONDS`
158-
159-
### 0.6.0
160-
161-
- Inboard 0.10.4 -> 0.37.0, including FastAPI 0.88
162-
- SQLAlchemy 1.3 -> 1.4
163-
- Authentication refresh token tables and schemas for long-term issuing of a new access token.
164-
- Postgresql 12 -> 14
165-
- Neo4j pinned to 5.2.0
166-
- Nuxt.js 2.5 -> 3.0
167-
- Pinia for state management (replaces Vuex)
168-
- Vee-Validate 3 -> 4
169-
- Tailwind 2.2 -> 3.2
170-
171141
[Historic changes from original](https://github.com/tiangolo/full-stack-fastapi-postgresql#release-notes)
172142

173143
## License

‎docs/authentication-guide.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
2. [Development and installation](development-guide.md)
55
3. [Deployment for production](deployment-guide.md)
66
4. [Authentication and magic tokens](authentication-guide.md)
7+
5. [Websockets for interactive communication](websocket-guide.md)
78

89
---
910

‎docs/deployment-guide.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
2. [Development and installation](development-guide.md)
55
3. [Deployment for production](deployment-guide.md)
66
4. [Authentication and magic tokens](authentication-guide.md)
7+
5. [Websockets for interactive communication](websocket-guide.md)
78

89
---
910

‎docs/development-guide.md

+20-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
2. [Development and installation](development-guide.md)
55
3. [Deployment for production](deployment-guide.md)
66
4. [Authentication and magic tokens](authentication-guide.md)
7+
5. [Websockets for interactive communication](websocket-guide.md)
78

89
---
910

@@ -90,6 +91,25 @@ And start them:
9091
docker-compose up -d
9192
```
9293

94+
By default, `backend` Python dependencies are managed with [Hatch](https://hatch.pypa.io/latest/). From `./backend/app/` you can install all the dependencies with:
95+
96+
```console
97+
$ hatch env prune
98+
$ hatch env create production
99+
```
100+
101+
Because Hatch doesn't have a version lock file (like Poetry), it is helpful to `prune` when you rebuild to avoid any sort of dependency hell. Then you can start a shell session with the new environment with:
102+
103+
```console
104+
$ hatch shell
105+
```
106+
107+
Make sure your editor uses the environment you just created with Hatch. For Visual Studio Code, from the shell, launch an appropriate development environment with:
108+
109+
```console
110+
$ code .
111+
```
112+
93113
**NOTE:** The Nuxt image does not automatically refresh while running in development mode. Any changes will need a rebuild. This gets tired fast, so it's easier to run Nuxt outside Docker and call through to the `backend` for API calls. You can then view the frontend at `http://localhost:3000` and the backend api endpoints at `http://localhost/redoc`. This problem won't be a concern in production.
94114

95115
Change into the `/frontend` folder, and:
@@ -99,8 +119,6 @@ yarn install
99119
yarn dev
100120
```
101121

102-
Be careful about the version of `Node.js` you're using. As of today (December 2022), the latest Node version supported by Nuxt is 16.18.1.
103-
104122
FastAPI `backend` updates will refresh automatically, but the `celeryworker` container must be restarted before changes take effect.
105123

106124
## Starting Jupyter Lab

‎docs/getting-started.md

+30-23
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
2. [Development and installation](development-guide.md)
55
3. [Deployment for production](deployment-guide.md)
66
4. [Authentication and magic tokens](authentication-guide.md)
7+
5. [Websockets for interactive communication](websocket-guide.md)
78

89
---
910

@@ -104,35 +105,41 @@ After using this generator, your new project will contain an extensive `README.m
104105

105106
See notes and [releases](https://github.com/whythawk/full-stack-fastapi-postgresql/releases). The last four release notes are listed here:
106107

107-
### 0.7.3
108-
- @nuxt/content 2.2.1 -> 2.4.3
109-
- Fixed: `@nuxt/content` default api, `/api/_content`, conflicts with the `backend` api url preventing content pages loading.
110-
- Documentation: Complete deployment guide in `DEPLOYMENT-README.md` (this has now been moved to `/docs`)
108+
## 0.8.2
111109

112-
### 0.7.2
113-
- Fixed: URLs for recreating project in generated `README.md`. PR [#15](https://github.com/whythawk/full-stack-fastapi-postgresql/pull/15) by @FranzForstmayr
114-
- Fixed: Absolute path for mount point in `docker-compose.override.yml`. PR [#16](https://github.com/whythawk/full-stack-fastapi-postgresql/pull/16) by @FranzForstmayr
115-
- Fixed: Login artifacts left over from before switch to magic auth. PR [#18](https://github.com/whythawk/full-stack-fastapi-postgresql/pull/18) by @turukawa and @FranzForstmayr
116-
- New: New floating magic login card. PR [#19](https://github.com/whythawk/full-stack-fastapi-postgresql/pull/19) by @turukawa
117-
- New: New site contact page. PR [#20](https://github.com/whythawk/full-stack-fastapi-postgresql/pull/20) by @turukawa
110+
Fixing [#39](https://github.com/whythawk/full-stack-fastapi-postgresql/issues/39), thanks to @a-vorobyoff:
118111

119-
### 0.7.1
112+
- Exposing port 24678 for Vite on frontend in development mode.
113+
- Ensuring Nuxt content on /api/_content doesn't interfere with backend /api/v routes.
114+
- Checking for password before hashing on user creation.
115+
- Updating generated README for Hatch (after Poetry deprecation).
116+
- Minor fixes.
120117

121-
- SQLAlchemy 1.4 -> 2.0
122-
- Nuxt.js 3.0 -> 3.2.2
123-
- Fixed: `tokenUrl` in `app/api/deps.py`. Thanks to @Choiuijin1125.
124-
- Fixed: SMTP options for TLS must be `ssl`. Thanks to @raouldo.
125-
- Fixed: `libgeos` is a dependency for `shapely` which is a dependency for `neomodel`, and which doesn't appear to be installed correctly on Macs. Thanks to @valsha and @Mocha-L.
126-
- Fixed: `frontend` fails to start in development. Thanks to @pabloapast and @dividor.
118+
### 0.8.1
127119

128-
### 0.7.0
120+
- Minor updates to Docker scripts for `build`.
129121

130-
- New feature: magic (email-based) login, with password fallback
131-
- New feature: Time-based One-Time Password (TOTP) authentication
132-
- Security enhancements to improve consistency, safety and reliability of the authentication process (see full description in the frontend app)
133-
- Requires one new `frontend` dependency: [QRcode.vue](https://github.com/scopewu/qrcode.vue)
122+
### 0.8.0
134123

135-
[Historic changes from original](https://github.com/tiangolo/full-stack-fastapi-postgresql#release-notes)
124+
- Updates to `frontend`, [#37](https://github.com/whythawk/full-stack-fastapi-postgresql/pull/37) by @turukawa:
125+
- `@nuxtjs/i18n` for internationalisation, along with language selection component.
126+
- `@vite-pwa/nuxt` along with button components for install and refreshing the app and service workers, and a CLI icon generator.
127+
- `@nuxtjs/robots` for simple control of `robots.txt` permissions from `nuxt.config.ts`.
128+
129+
### 0.7.4
130+
131+
- Updates: Complete update of stack to latest long-term releases. [#35](https://github.com/whythawk/full-stack-fastapi-postgresql/pull/35) by @turukawa, review by @br3ndonland
132+
- `frontend`:
133+
- Node 16 -> 18
134+
- Nuxt 3.2 -> 3.6.5
135+
- Latest Pinia requires changes in stores, where imports are not required (cause actual errors), and parameter declaration must happen in functions.
136+
- `backend` and `celeryworker`:
137+
- Python 3.9 -> 3.11
138+
- FastAPI 0.88 -> 0.99 (Inboard 0.37 -> 0.51)
139+
- Poetry -> Hatch
140+
- Postgres 14 -> 15
141+
- Fixed: Updated token url in deps.py [#29](https://github.com/whythawk/full-stack-fastapi-postgresql/pull/29) by @vusa
142+
- Docs: Reorganised documentation [#21](https://github.com/whythawk/full-stack-fastapi-postgresql/pull/21) by @turukawa
136143

137144
## License
138145

‎docs/websocket-guide.md

+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# Websockets for interactive communication
2+
3+
[Websockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) permit synchronous interactive communication between a user's browser and a server. A series of messages are sent between each end, triggering response events.
4+
5+
While Websockets support multi-user sessions, this documentation is mainly focused on a single-user session.
6+
7+
---
8+
9+
1. [Getting started](getting-started.md)
10+
2. [Development and installation](development-guide.md)
11+
3. [Deployment for production](deployment-guide.md)
12+
4. [Authentication and magic tokens](authentication-guide.md)
13+
5. [Websockets for interactive communication](websocket-guide.md)
14+
15+
---
16+
## Contents
17+
18+
- [Why Websockets?](#why-websockets)
19+
- [High-level architecture and workflow](#high-level-architecture-and-workflow)
20+
- [Requirements](#requirements)
21+
- [Setting up the Nuxt `frontend`](#setting-up-the-nuxt-frontend)
22+
- [Setting up the FastAPI `backend`](#setting-up-the-fastapi-backend)
23+
24+
## Why Websockets?
25+
26+
Web applications sessions are not persistent. You can maintain state at the back- _or_ frontend, but not both simultaneously. It's great that `Nuxt Pinia` allows your browser to store what you've been doing, but that will need to be communicated via your API to the backend on every page change.
27+
28+
There are ways around this, such as automatically polling the API (known as [long polling](https://ably.com/topic/long-polling)), but Websockets create a bidirectional session between the front- and backends, allowing you to run a synchronous interactive process.
29+
30+
This is great for interactive chat services, or where your app offers complex software which is impractical (or outright impossible) to run in a browser.
31+
32+
Depending on the popularity of your app, concurrency may become a problem. Stateless polling means you can have millions of users on relatively limited infrastructure, because they load information, and then read it. People are using your app simultaneously, not polling your infrastructure simultaneously.
33+
34+
Websockets are active sessions, so be sparing with the amount of information you send back and forth.
35+
36+
## High-level architecture and workflow
37+
38+
The following general conditions apply:
39+
40+
- If a session requires user-authentication, then you need to manually perform this. You may be used to FastAPI routes handling this for you normally, but Websockets don't.
41+
- Messages sent between front- and backend are `JSON-encoded`. If you use variables that aren't easily stringified (Pydantic, for example, doesn't automatically stringify UUIDs), you'll need to deliberately check for this.
42+
43+
Here's how the workflow tends to play out:
44+
45+
- `frontend` - initialise a socket by opening a session and sending an initial payload `request`, which may include a user token (if authentication is required).
46+
- `backend` - receive a socket `request`, validate the session (user or other), send a `response` to the `frontend`, and then enter a `while True` loop to keep the session open and keep listening for `requests`.
47+
- `frontend` - each `response` triggers an event, updating the view.
48+
- `frontend` - send an instruction to close the socket when the user ends the sessions or, as fallback, when the user changes the page.
49+
- `backend` - can also end the session based on user activity.
50+
51+
If the user is joining a multi-user session, then each update to the session is communicated to all users.
52+
53+
## Requirements
54+
55+
- [FastAPI](https://fastapi.tiangolo.com/advanced/websockets/) requires the installation of the WebSockets library:
56+
57+
```
58+
pip install websockets
59+
```
60+
61+
- There are multiple JavaScript libraries for Websockets, but I like [WebSocketAs Promised](https://github.com/vitalets/websocket-as-promised#readme):
62+
63+
```
64+
yarn install websocket-as-promised
65+
```
66+
67+
## Setting up the Nuxt `frontend`
68+
69+
The API `backend` is reached at `ws://localhost/api/v1` (or `wss://<your-domain>/api/v1` in production).
70+
71+
Create an appropriate `websocketAPI.ts` file:
72+
73+
```
74+
import WebSocketAsPromised from "websocket-as-promised"
75+
import { apiCore } from "./core"
76+
77+
export const apiSockets = {
78+
socketRequest() {
79+
return new WebSocketAsPromised(`${apiCore.wsurl()}/socket`, {
80+
packMessage: (data) => JSON.stringify(data),
81+
unpackMessage: (data) => JSON.parse(data as string),
82+
})
83+
},
84+
}
85+
```
86+
87+
Then, in the relevant `page.ts` (or component), you create a `websocket` variable (`wsp`) and attach a `watcher` to it so that you can respond to events as it is updated:
88+
89+
```
90+
<script setup lang="ts">
91+
import WebSocketAsPromised from "websocket-as-promised"
92+
import { IKeyable, ISocketRequest, ISocketResponse } from "@/interfaces"
93+
import { apiSockets } from "@/api"
94+
import { useAuthStore } from "@/stores"
95+
96+
// SETUP
97+
let wsp = {} as WebSocketAsPromised
98+
const authStore = useAuthStore()
99+
const streaming = ref(false)
100+
101+
onMounted(async () => {
102+
await authStore.refreshTokens()
103+
await initialiseSocket()
104+
})
105+
106+
async function initialiseSocket() {
107+
wsp = apiSockets.socketRequest()
108+
await wsp.open()
109+
wsp.onUnpackedMessage.addListener((data) => watchResponseSocket(data))
110+
// Deliberately sending a user token to authenticate the session
111+
const jsonData: IKeyable = {
112+
token: authStore.authTokens.token
113+
}
114+
wsp.sendPacked(jsonData)
115+
streaming.value = true
116+
}
117+
118+
function closeSocket() {
119+
wsp.onUnpackedMessage.removeListener((data) => watchResponseSocket(data))
120+
if (streaming.value) {
121+
wsp.close()
122+
streaming.value = false
123+
}
124+
}
125+
126+
onBeforeRouteLeave((to, from, next) => {
127+
closeSocket()
128+
next()
129+
})
130+
131+
// SESSION
132+
async function watchResponseSocket(response: ISocketResponse) {
133+
// response: { state, data, error }
134+
console.log("response: ", response.state, response.data)
135+
switch (response.state) {
136+
case "initialised":
137+
// User session is authenticated and you can now launch the process
138+
await watchRequestSocket({
139+
state: "startThings",
140+
data: {}
141+
})
142+
break
143+
case "somethingHappened":
144+
// Do stuff
145+
break
146+
case "somethingElse":
147+
// Do some other stuff
148+
break
149+
case "error":
150+
toast.addNotice({
151+
title: "Some error",
152+
content: `Error: ${response.error}`,
153+
icon: "error"
154+
})
155+
break
156+
}
157+
}
158+
159+
async function watchRequestSocket(request: ISocketRequest) {
160+
// request: { state, data }
161+
switch (request.state) {
162+
case "something":
163+
// Request-specific options
164+
break
165+
default:
166+
sendSocketRequest(request.state, request.data)
167+
}
168+
}
169+
170+
function sendSocketRequest(state: string, data: IKeyable) {
171+
try {
172+
const payload: ISocketRequest = {
173+
state,
174+
data
175+
}
176+
wsp.sendPacked(payload)
177+
} catch (e) {
178+
console.log(e)
179+
// closeSocket()
180+
}
181+
}
182+
</script>
183+
```
184+
185+
The `response` is a `switch` statement which identifies and responds to the appropriate event.
186+
187+
## Setting up the FastAPI `backend`
188+
189+
At the `backend` you already have a [sockets.py](https://github.com/whythawk/full-stack-fastapi-postgresql/blob/0.8.2/%7B%7Bcookiecutter.project_slug%7D%7D/backend/app/app/api/sockets.py) which handles serialising and deserialising the Websocket requests and responses. Now you create a route in `/endpoints`:
190+
191+
```
192+
from fastapi import APIRouter, Depends, WebSocket, HTTPException, WebSocketException
193+
from starlette.websockets import WebSocketDisconnect
194+
from websockets.exceptions import ConnectionClosedError
195+
from app import crud, models, schema_types, schemas
196+
from app.api import deps, sockets
197+
198+
router = APIRouter()
199+
200+
@router.websocket("/socket")
201+
async def some_websocket_session(*, db: Session = Depends(deps.get_db), websocket: WebSocket):
202+
current_user = None
203+
initialised = False
204+
success = False
205+
# 1. Open the socket and validate current user
206+
await websocket.accept()
207+
request = await sockets.receive_request(websocket=websocket)
208+
response = {"state": "error", "error": "Could not validate credentials."}
209+
if request.get("token"):
210+
try:
211+
current_user = deps.get_active_websocket_user(db=db, token=request["token"])
212+
response = {"state": "initialised", "data": {}}
213+
except ValidationError:
214+
pass
215+
success = await sockets.send_response(websocket=websocket, response=response)
216+
if response["state"] == "initialised" and success:
217+
try:
218+
while True and success:
219+
# LOOP #################################################################
220+
request = await sockets.receive_request(websocket=websocket)
221+
if not request:
222+
break
223+
state = request.get("state")
224+
data = request.get("data", {})
225+
data = sockets.sanitize_data_request(data)
226+
response = {"state": state}
227+
try:
228+
# ALL THE STATES ###################################################
229+
if state == "startThings":
230+
# Do some stuff
231+
data = {"something": "yes, something"}
232+
response["data"] = data
233+
initialised = True
234+
# SAVE AND CLOSE THE SESSION #######################################
235+
if state == "save" and initialised:
236+
# This will close the socket, if it succeeds
237+
response["data"] = {}
238+
break
239+
except ValidationError as e:
240+
response = {"state": "error", "error": e}
241+
success = await sockets.send_response(websocket=websocket, response=response)
242+
# LOOP #################################################################
243+
except (WebSocketDisconnect, WebSocketException, ConnectionClosedError) as e:
244+
response = {"state": "error", "error": e}
245+
try:
246+
await sockets.send_response(websocket=websocket, response=response)
247+
await websocket.close(code=1000)
248+
except (WebSocketDisconnect, ConnectionClosedError, RuntimeError, WebSocketException):
249+
pass
250+
```
251+
252+
And that's - very simplistically - basically it.

0 commit comments

Comments
 (0)
Please sign in to comment.