# Sessions A sessions module for web projects. ## Usage The sessions module provides an implementation 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 integrates 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, to store the session data. 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]' }) or { return ctx.server_error('could not save session data, please try again') } 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 }) or { return ctx.server_error('could not save session data, please try again') } 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) or { return ctx.server_error('could not logout, please try again') } 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 mitigate 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.