Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
x.vweb: add new sessions module (#20642)
- Loading branch information
Showing
11 changed files
with
1,102 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,335 @@ | ||
# Sessions | ||
|
||
A sessions module for web projects. | ||
|
||
## Usage | ||
|
||
The sessions module provides an implemention for [session stores](#custom-stores). | ||
The session store handles the saving, storing and retrieving of data. You can | ||
either use a store directly yourself, or you can use the `session.Sessions` struct | ||
which is easier to use since it also handles session verification and intergrates nicely | ||
with vweb. | ||
|
||
If you want to use `session.Sessions` in your web app the session id's will be | ||
stored using cookies. The best way to get started is to follow the | ||
[getting started](#getting-started) section. | ||
|
||
Otherwise have a look at the [advanced usage](#advanced-usage) section. | ||
|
||
## Getting Started | ||
|
||
The examples in this section use `x.vweb`. See the [advanced usage](#advanced-usage) section | ||
for examples without `x.vweb`. | ||
|
||
To start using sessions in vweb embed `sessions.CurrentSession` on the | ||
Context struct and add `sessions.Sessions` to the app struct. We must also pass the type | ||
of our session data. | ||
|
||
For any further example code we will use the `User` struct. | ||
**Example:** | ||
```v ignore | ||
import x.sessions | ||
import x.vweb | ||
pub struct User { | ||
pub mut: | ||
name string | ||
verified bool | ||
} | ||
pub struct Context { | ||
vweb.Context | ||
// By embedding the CurrentSession struct we can directly access the current session id | ||
// and any associated session data. Set the session data type to `User` | ||
sessions.CurrentSession[User] | ||
} | ||
pub struct App { | ||
pub mut: | ||
// this struct contains the store that holds all session data it also provides | ||
// an easy way to manage sessions in your vweb app. Set the session data type to `User` | ||
sessions &sessions.Sessions[User] | ||
} | ||
``` | ||
|
||
Next we need to create the `&sessions.Sessions[User]` instance for our app. This | ||
struct provides functionality to easier manage sessions in a vweb app. | ||
|
||
### Session Stores | ||
|
||
To create `sessions.Sessions` We must specify a "store" which handles the session data. | ||
Currently vweb provides two options for storing session data: | ||
|
||
1. The `MemoryStore[T]` stores session data in memory only using the `map` datatype. | ||
2. The `DBStore[T]` stores session data in a database by encoding the session data to JSON. | ||
It will create the table `DBStoreSessions` in your database where the session data will be stored. | ||
|
||
It is possible to create your own session store, see [custom stores](#custom-stores). | ||
|
||
### Starting the App | ||
|
||
For this example we will use the memory store. | ||
|
||
**Example:** | ||
```v ignore | ||
fn main() { | ||
mut app := &App{ | ||
store: sessions.MemoryStore[User]{} | ||
// use your own secret which will be used to verify session id's | ||
secret: 'my secret'.bytes() | ||
} | ||
vweb.run[App, Context](mut app, 8080) | ||
} | ||
``` | ||
|
||
### Middleware | ||
|
||
The `sessions.vweb2_middleware` module provides a middleware handler. This handler will execute | ||
before your own route handlers and will verify the current session and fetch any associated | ||
session data and load it into `sessions.CurrentSession`, which is embedded on the Context struct. | ||
|
||
> **Note:** | ||
> It is recommended to use the middleware, so the sessions are always verfied | ||
> and loaded correctly. | ||
**Example:** | ||
```v ignore | ||
// add this import at the top of your file | ||
import x.sessions.vweb2_middleware | ||
pub struct App { | ||
// embed the Middleware struct from vweb | ||
vweb.Middleware[Context] | ||
pub mut: | ||
// this struct contains the store that holds all session data it also provides | ||
// an easy way to manage sessions in your vweb app. Set the session data type to `User` | ||
sessions &sessions.Sessions[User] | ||
} | ||
fn main() { | ||
mut app := &App{ | ||
store: sessions.MemoryStore[User]{} | ||
// use your own secret which will be used to verify session id's | ||
secret: 'my secret'.bytes() | ||
} | ||
// register the sessions middleware | ||
app.use(vweb2_middleware.create[User, Context](mut app.sessions)) | ||
vweb.run[App, Context](mut app, 8080) | ||
} | ||
``` | ||
|
||
You can now start using sessions with vweb! | ||
|
||
### Usage in endpoint handlers | ||
|
||
#### Using Session Data | ||
|
||
Because `sessions.CurrentSession` is embedded on the Context struct we can directly | ||
access any session data via `ctx.session_data`. This field is an option, it will be `none` | ||
if no data is set. | ||
|
||
**Example:** | ||
```v ignore | ||
pub fn (app &App) index(mut ctx Context) vweb.Result { | ||
// check if a user is logged in | ||
if user := ctx.session_data { | ||
return ctx.text('Welcome ${user.name}! Verification status: ${user.verified}') | ||
} else { | ||
// user is not logged in | ||
return ctx.text('You are not logged in :(') | ||
} | ||
} | ||
``` | ||
|
||
#### Saving / updating session data | ||
|
||
You can use the `save` method to update and save any session data. | ||
|
||
When the user logs in, the `save` method is called and a new session id is generated | ||
and set as cookie. Assuming there wasn't already a session going on. If you want to | ||
be sure that a new session id is generated when you save data, you can use the `resave` | ||
method. This method will save the data and *always* set a new session id. | ||
|
||
**Example:** | ||
```v ignore | ||
pub fn (mut app App) login(mut ctx Context) vweb.Result { | ||
// set a session id cookie and save data for the new user | ||
app.sessions.save(mut ctx, User{ | ||
name: '[no name provided]' | ||
}) | ||
return ctx.text('You are now logged in!') | ||
} | ||
``` | ||
|
||
The following endpoint checks if a session exists, if it doesn't inform the | ||
user that they need to login. | ||
|
||
If a session does exists the users name is updated to the `name` query parameter, | ||
you can use this route via `http://localhost:8080/save?name=myname`. And if the | ||
query parameter is not passed an error 400 (bad request) is returned. | ||
|
||
**Example:** | ||
```v ignore | ||
pub fn (mut app App) save(mut ctx Context) vweb.Result { | ||
// check if there is a session | ||
app.sessions.get(ctx) or { return ctx.request_error('You are not logged in :(') } | ||
if name := ctx.query['name'] { | ||
// update the current user | ||
app.sessions.save(mut ctx, User{ | ||
name: name | ||
}) | ||
return ctx.redirect('/', typ: .see_other) | ||
} else { | ||
// send HTTP 400 error | ||
return ctx.request_error('query parameter "name" must be present!') | ||
} | ||
} | ||
``` | ||
|
||
#### Destroying data / logging out | ||
|
||
If a user logs out you can use the `logout` method to destroy the session data and | ||
clear the session id cookie. If you only want to destroy the session data use the `destroy` | ||
method. | ||
|
||
**Example:** | ||
```v ignore | ||
pub fn (mut app App) logout(mut ctx Context) vweb.Result { | ||
app.sessions.logout(mut ctx) | ||
return ctx.text('You are now logged out!') | ||
} | ||
``` | ||
|
||
### Configuration | ||
|
||
Change the `cookie_options` field to modify how the session cookie is stored. | ||
|
||
**Example:** | ||
```v ignore | ||
mut app := &App{ | ||
sessions: &sessions.Sessions[User]{ | ||
// ... | ||
cookie_options: sessions.CookieOptions{ | ||
// cookie can only be stored on an HTTPS site. | ||
secure: true | ||
} | ||
} | ||
} | ||
``` | ||
|
||
#### Mag-age | ||
|
||
By default the expiration date of a session is 30 days. You can change this by | ||
setting the `max_age` field. If `max_age = 0`, then session expiration times are not | ||
checked and sessions will be stored forever until they are destroyed. | ||
|
||
#### Pre-sessions | ||
|
||
By default a session cookie is only generated when you call `save`, or `resave`. | ||
By setting `save_uninitialized` to `true` a session cookie will always be set, | ||
even if there is no data for the session yet. This is useful when you need session | ||
data to be always available. | ||
|
||
Or, for example, you could use pre-sessions to mittigate login-csrf, | ||
since you can bind a csrf-token to the "pre-session" id. Then when the user logs | ||
in, you can set a new session id with `resave`.. | ||
|
||
## Advanced Usage | ||
|
||
If you want to store session id's in another manner than cookies, or if you want | ||
to use this sessions module outside of vweb, the easiest way is to create an | ||
instance of a `Store` and directly interact with it. | ||
|
||
First we create an instance of the `MemoryStore` and pass the user struct as data type. | ||
**Example:** | ||
```v | ||
import x.sessions | ||
const secret = 'my secret'.bytes() | ||
pub struct User { | ||
pub mut: | ||
name string | ||
verified bool | ||
} | ||
fn main() { | ||
mut store := sessions.MemoryStore[User]{} | ||
user := User{ | ||
name: 'vaesel' | ||
} | ||
} | ||
``` | ||
|
||
### Generating and validating session id's | ||
|
||
The session module provides a function for generating a new signed session id | ||
and for verifying a signed session id. You can ofcourse generate your own session id's. | ||
|
||
**Example:** | ||
```v ignore | ||
// fn main | ||
// generate a new session id and sign it | ||
session_id, signed_session_id := sessions.new_session_id(secret) | ||
// save session data to our store | ||
store.set(session_id, user) | ||
// get a normal session id from the signed version and verify it | ||
verified_session_id, valid := sessions.verify_session_id(signed_session_id, secret) | ||
assert verified_session_id == session_id && valid == true | ||
``` | ||
|
||
We can retrieve the saved user and verify that the data we saved can be retrieved | ||
from the verified session id. | ||
|
||
**Example:** | ||
```v ignore | ||
// fn main | ||
// pass `max_age = 0` to ignore the expiration time. | ||
if saved_user := store.get(verified_session_id, 0) { | ||
assert user == saved_user | ||
println('Retrieved a valid user! ${saved_user}') | ||
} else { | ||
println(':(') | ||
} | ||
``` | ||
|
||
## Custom Stores | ||
|
||
You can easily create your own custom store in order to control how session data is | ||
stored and retrieved. Each session store needs to implement the `Store[T]` interface. | ||
|
||
```v ignore | ||
pub interface Store[T] { | ||
mut: | ||
// get the current session data if the id exists and if it's not expired. | ||
// If the session is expired, any associated data should be destroyed. | ||
// If `max_age=0` the store will not check for expiration of the session. | ||
get(sid string, max_age time.Duration) ?T | ||
// destroy session data for `sid` | ||
destroy(sid string) | ||
// set session data for `sid` | ||
set(sid string, val T) | ||
} | ||
// get data from all sessions, optional to implement | ||
pub fn (mut s Store) all[T]() []T { | ||
return []T{} | ||
} | ||
// clear all session data, optional to implement | ||
pub fn (mut s Store) clear[T]() {} | ||
``` | ||
|
||
Only the `get`, `destroy` and `set` methods are required to implement. | ||
|
||
### Session Expire time | ||
|
||
The `max_age` argument in `get` can be used to check whether the session is still valid. | ||
The database and memory store both check the expiration time from the time the session data | ||
first inserted. But if `max_age = 0`, the stores will not check for expiration time. |
Oops, something went wrong.