2022-08-20 12:06:24 +03:00
|
|
|
# vweb - the V Web Server
|
2019-07-29 20:46:26 +03:00
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
A simple yet powerful web server with built-in routing, parameter handling, templating, and other
|
|
|
|
features.
|
|
|
|
The [gitly](https://gitly.org/) site is based on vweb.
|
2019-12-10 00:16:39 +03:00
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
**_Some features may not be complete, and have some bugs._**
|
2019-07-29 20:46:26 +03:00
|
|
|
|
2023-04-23 03:37:15 +03:00
|
|
|
## Quick Start
|
2023-01-08 19:33:17 +03:00
|
|
|
Just run **`v new <name> web`** in your terminal
|
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
## Features
|
2019-07-30 22:15:17 +03:00
|
|
|
|
2021-05-15 05:53:25 +03:00
|
|
|
- **Very fast** performance of C on the web.
|
|
|
|
- **Small binary** hello world website is <100 KB.
|
2022-08-20 12:06:24 +03:00
|
|
|
- **Easy to deploy** just one binary file that also includes all templates. No need to install any
|
|
|
|
dependencies.
|
|
|
|
- **Templates are precompiled** all errors are visible at compilation time, not at runtime.
|
2023-04-23 03:37:15 +03:00
|
|
|
- **Multithreaded** by default
|
2022-08-20 12:06:24 +03:00
|
|
|
|
|
|
|
### Examples
|
2021-05-15 05:53:25 +03:00
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
There are some examples
|
|
|
|
that can be explored [here](https://github.com/vlang/v/tree/master/examples/vweb).
|
2021-05-15 05:53:25 +03:00
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
And others like:
|
2019-07-30 22:15:17 +03:00
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
- [vweb_orm_jwt](https://github.com/vlang/v/tree/master/examples/vweb_orm_jwt) (back-end)
|
|
|
|
- [vorum](https://github.com/vlang/vorum) (front-end)
|
|
|
|
- [gitly](https://github.com/vlang/gitly) (full-stack)
|
|
|
|
|
|
|
|
**Front-end getting start example**
|
|
|
|
`src/main.v`
|
2019-07-29 19:50:25 +03:00
|
|
|
|
2020-11-18 20:28:28 +03:00
|
|
|
```v ignore
|
2022-08-20 12:06:24 +03:00
|
|
|
module main
|
|
|
|
|
|
|
|
import vweb
|
|
|
|
import os
|
|
|
|
|
|
|
|
struct App {
|
|
|
|
vweb.Context
|
2019-07-29 19:50:25 +03:00
|
|
|
}
|
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
struct Object {
|
|
|
|
title string
|
|
|
|
description string
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
vweb.run_at(new_app(), vweb.RunParams{
|
|
|
|
port: 8081
|
|
|
|
}) or { panic(err) }
|
|
|
|
}
|
|
|
|
|
|
|
|
fn new_app() &App {
|
|
|
|
mut app := &App{}
|
|
|
|
// makes all static files available.
|
|
|
|
app.mount_static_folder_at(os.resource_abs_path('.'), '/')
|
|
|
|
return app
|
2019-07-29 19:50:25 +03:00
|
|
|
}
|
2022-08-20 12:06:24 +03:00
|
|
|
|
|
|
|
['/']
|
|
|
|
pub fn (mut app App) page_home() vweb.Result {
|
|
|
|
// all this constants can be accessed by src/templates/page/home.html file.
|
|
|
|
page_title := 'V is the new V'
|
|
|
|
v_url := 'https://github.com/vlang/v'
|
|
|
|
|
|
|
|
list_of_object := [
|
|
|
|
Object{
|
|
|
|
title: 'One good title'
|
|
|
|
description: 'this is the first'
|
|
|
|
},
|
|
|
|
Object{
|
|
|
|
title: 'Other good title'
|
|
|
|
description: 'more one'
|
|
|
|
},
|
|
|
|
]
|
|
|
|
// $vweb.html() in `<folder>_<name> vweb.Result ()` like this
|
|
|
|
// render the `<name>.html` in folder `./templates/<folder>`
|
|
|
|
return $vweb.html()
|
|
|
|
}
|
|
|
|
|
2019-07-29 19:50:25 +03:00
|
|
|
```
|
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
`$vweb.html()` compiles an HTML template into V during compilation, and embeds the resulting code
|
|
|
|
into the current action.
|
|
|
|
|
|
|
|
That means that the template automatically has access to that action's entire environment.
|
|
|
|
|
|
|
|
`src/templates/page/home.html`
|
2019-07-29 19:50:25 +03:00
|
|
|
|
2019-07-29 20:46:26 +03:00
|
|
|
```html
|
2022-08-20 12:06:24 +03:00
|
|
|
<html>
|
|
|
|
<header>
|
|
|
|
<title>${page_title}</title>
|
|
|
|
@css 'src/templates/page/home.css'
|
|
|
|
</header>
|
|
|
|
<body>
|
|
|
|
<h1 class="title">Hello, Vs.</h1>
|
|
|
|
@for var in list_of_object
|
|
|
|
<div>
|
|
|
|
<a href="${v_url}">${var.title}</a>
|
|
|
|
<span>${var.description}</span>
|
|
|
|
</div>
|
|
|
|
@end
|
|
|
|
<div>@include 'component.html'</div>
|
|
|
|
</body>
|
|
|
|
</html>
|
2019-07-29 19:50:25 +03:00
|
|
|
```
|
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
`src/templates/page/component.html`
|
2019-07-29 19:50:25 +03:00
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
```html
|
|
|
|
<div>This is a component</div>
|
|
|
|
```
|
2019-07-29 20:46:26 +03:00
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
`src/templates/page/home.css`
|
2019-07-29 20:46:26 +03:00
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
```css
|
|
|
|
h1.title {
|
|
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
|
|
color: #3b7bbf;
|
|
|
|
}
|
|
|
|
```
|
2021-03-03 15:39:04 +03:00
|
|
|
|
2022-09-29 14:29:02 +03:00
|
|
|
V supports [Template directives](/vlib/v/TEMPLATES.md) like
|
2022-08-20 12:06:24 +03:00
|
|
|
`@css`, `@js` for static files in \<path\>
|
|
|
|
`@if`, `@for` for conditional and loop
|
|
|
|
and
|
|
|
|
`@include` to include html components.
|
2021-05-15 05:53:25 +03:00
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
## Deploying vweb apps
|
2021-03-03 15:39:04 +03:00
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
Everything, including HTML templates, is in one binary file. That's all you need to deploy.
|
|
|
|
|
|
|
|
## Getting Started
|
|
|
|
|
|
|
|
To start with vweb, you have to import the module `vweb` and define a struct to hold vweb.Context
|
|
|
|
(and any other variables your program will need).
|
|
|
|
The web server can be started by calling `vweb.run(&App{}, port)` or `vweb.run(&App{}, RunParams)`
|
2021-03-03 15:39:04 +03:00
|
|
|
|
|
|
|
**Example:**
|
2021-05-15 05:53:25 +03:00
|
|
|
|
2021-03-03 15:39:04 +03:00
|
|
|
```v ignore
|
|
|
|
import vweb
|
|
|
|
|
|
|
|
struct App {
|
|
|
|
vweb.Context
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
2021-05-17 08:51:49 +03:00
|
|
|
vweb.run(&App{}, 8080)
|
2022-08-20 12:06:24 +03:00
|
|
|
// // or
|
|
|
|
// vweb.run_at(new_app(), vweb.RunParams{
|
|
|
|
// host: 'localhost'
|
|
|
|
// port: 8099
|
|
|
|
// family: .ip
|
|
|
|
// }) or { panic(err) }
|
2021-03-03 15:39:04 +03:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
### Defining endpoints
|
2021-05-15 05:53:25 +03:00
|
|
|
|
2021-03-03 15:39:04 +03:00
|
|
|
To add endpoints to your web server, you have to extend the `App` struct.
|
|
|
|
For routing you can either use auto-mapping of function names or specify the path as an attribute.
|
|
|
|
The function expects a response of the type `vweb.Result`.
|
|
|
|
|
|
|
|
**Example:**
|
2021-05-15 05:53:25 +03:00
|
|
|
|
2021-03-03 15:39:04 +03:00
|
|
|
```v ignore
|
|
|
|
// This endpoint can be accessed via http://localhost:port/hello
|
|
|
|
fn (mut app App) hello() vweb.Result {
|
|
|
|
return app.text('Hello')
|
|
|
|
}
|
|
|
|
|
|
|
|
// This endpoint can be accessed via http://localhost:port/foo
|
|
|
|
["/foo"]
|
|
|
|
fn (mut app App) world() vweb.Result {
|
|
|
|
return app.text('World')
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
#### - HTTP verbs
|
|
|
|
|
|
|
|
To use any HTTP verbs (or methods, as they are properly called),
|
|
|
|
such as `[post]`, `[get]`, `[put]`, `[patch]` or `[delete]`
|
|
|
|
you can simply add the attribute before the function definition.
|
2021-03-03 15:39:04 +03:00
|
|
|
|
|
|
|
**Example:**
|
2021-05-15 05:53:25 +03:00
|
|
|
|
2021-03-03 15:39:04 +03:00
|
|
|
```v ignore
|
|
|
|
[post]
|
|
|
|
fn (mut app App) world() vweb.Result {
|
|
|
|
return app.text('World')
|
|
|
|
}
|
2022-08-20 12:06:24 +03:00
|
|
|
|
|
|
|
['/product/create'; post]
|
|
|
|
fn (mut app App) create_product() vweb.Result {
|
|
|
|
return app.text('product')
|
|
|
|
}
|
2021-03-03 15:39:04 +03:00
|
|
|
```
|
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
#### - Parameters
|
|
|
|
|
2023-02-16 12:43:39 +03:00
|
|
|
Parameters are passed directly in endpoint route using colon sign `:` and received using the same
|
2022-08-20 12:06:24 +03:00
|
|
|
name at function
|
|
|
|
To pass a parameter to an endpoint, you simply define it inside an attribute, e. g.
|
|
|
|
`['/hello/:user]`.
|
2021-03-03 15:39:04 +03:00
|
|
|
After it is defined in the attribute, you have to add it as a function parameter.
|
|
|
|
|
|
|
|
**Example:**
|
2021-05-15 05:53:25 +03:00
|
|
|
|
2021-03-03 15:39:04 +03:00
|
|
|
```v ignore
|
2022-08-20 12:06:24 +03:00
|
|
|
vvvv
|
|
|
|
['/hello/:user'] vvvv
|
2021-03-03 15:39:04 +03:00
|
|
|
fn (mut app App) hello_user(user string) vweb.Result {
|
|
|
|
return app.text('Hello $user')
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2021-04-09 19:17:33 +03:00
|
|
|
You have access to the raw request data such as headers
|
2021-03-03 15:39:04 +03:00
|
|
|
or the request body by accessing `app` (which is `vweb.Context`).
|
|
|
|
If you want to read the request body, you can do that by calling `app.req.data`.
|
2021-05-15 05:53:25 +03:00
|
|
|
To read the request headers, you just call `app.req.header` and access the
|
2022-08-28 09:55:18 +03:00
|
|
|
header you want example. `app.req.header.get(.content_type)`. See `struct Header`
|
2021-05-15 05:53:25 +03:00
|
|
|
for all available methods (`v doc net.http Header`).
|
2022-08-28 09:55:18 +03:00
|
|
|
It has, too, fields for the `query`, `form`, `files`.
|
2022-08-20 12:06:24 +03:00
|
|
|
|
2022-08-28 09:55:18 +03:00
|
|
|
#### - Query
|
|
|
|
To handle the query context, you just need use the `query` field
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v
|
|
|
|
module main
|
|
|
|
|
|
|
|
import vweb
|
|
|
|
|
|
|
|
struct App {
|
|
|
|
vweb.Context
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
vweb.run(&App{}, 8081)
|
|
|
|
}
|
|
|
|
|
|
|
|
['/user'; get]
|
|
|
|
pub fn (mut app App) controller_get_user_by_id() vweb.Result {
|
|
|
|
// http://localhost:3000/user?q=vpm&order_by=desc => { 'q': 'vpm', 'order_by': 'desc' }
|
|
|
|
return app.text(app.query.str())
|
|
|
|
}
|
|
|
|
```
|
2023-05-29 03:11:10 +03:00
|
|
|
#### - Host
|
|
|
|
To restrict an endpoint to a specific host, you can use the `host` attribute
|
2023-05-30 15:22:23 +03:00
|
|
|
followed by a colon `:` and the host name. You can test the Host feature locally
|
|
|
|
by adding a host to the "hosts" file of your device.
|
2023-05-29 03:11:10 +03:00
|
|
|
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
['/'; host: 'example.com']
|
|
|
|
pub fn (mut app App) hello_web() vweb.Result {
|
|
|
|
return app.text('Hello World')
|
|
|
|
}
|
|
|
|
|
|
|
|
['/'; host: 'api.example.org']
|
|
|
|
pub fn (mut app App) hello_api() vweb.Result {
|
|
|
|
return app.text('Hello API')
|
|
|
|
}
|
2023-05-30 15:22:23 +03:00
|
|
|
|
|
|
|
// define the handler without a host attribute last if you have conflicting paths.
|
|
|
|
['/']
|
|
|
|
pub fn (mut app App) hello_others() vweb.Result {
|
|
|
|
return app.text('Hello Others')
|
|
|
|
}
|
2023-05-29 03:11:10 +03:00
|
|
|
```
|
|
|
|
|
2023-05-30 15:22:23 +03:00
|
|
|
You can also [create a controller](#hosts) to handle all requests from a specific
|
|
|
|
host in one app.
|
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
### Middleware
|
|
|
|
|
2023-03-26 02:57:42 +03:00
|
|
|
Vweb has different kinds of middleware.
|
|
|
|
The `before_request()` method is always called before every request before any
|
|
|
|
other middleware is processed. You could use it to check user session cookies or to add a header.
|
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
pub fn (mut app App) before_request() {
|
|
|
|
app.user_id = app.get_cookie('id') or { '0' }
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2023-03-26 02:57:42 +03:00
|
|
|
Middleware functions can be passed directly when creating an App instance and is
|
|
|
|
executed when the url starts with the defined key.
|
|
|
|
|
|
|
|
In the following example, if a user navigates to `/path/to/test` the middleware
|
|
|
|
is executed in the following order: `middleware_func`, `other_func`, `global_middleware`.
|
|
|
|
The middleware is executed in the same order as they are defined and if any function in
|
|
|
|
the chain returns `false` the propogation is stopped.
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
```v
|
|
|
|
module main
|
|
|
|
|
|
|
|
import vweb
|
|
|
|
|
|
|
|
struct App {
|
|
|
|
vweb.Context
|
|
|
|
middlewares map[string][]vweb.Middleware
|
|
|
|
}
|
|
|
|
|
|
|
|
fn new_app() &App {
|
|
|
|
mut app := &App{
|
|
|
|
middlewares: {
|
|
|
|
// chaining is allowed, middleware will be evaluated in order
|
|
|
|
'/path/to/': [middleware_func, other_func]
|
|
|
|
'/': [global_middleware]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// do stuff with app
|
|
|
|
// ...
|
|
|
|
return app
|
|
|
|
}
|
|
|
|
|
|
|
|
fn middleware_func(mut ctx vweb.Context) bool {
|
|
|
|
// ...
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
fn other_func(mut ctx vweb.Context) bool {
|
|
|
|
// ...
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
fn global_middleware(mut ctx vweb.Context) bool {
|
|
|
|
// ...
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
Middleware functions will be of type `vweb.Middleware` and are not methods of App,
|
|
|
|
so they could also be imported from other modules.
|
|
|
|
```v ignore
|
|
|
|
pub type Middleware = fn (mut Context) bool
|
|
|
|
```
|
|
|
|
|
|
|
|
Middleware can also be added to route specific functions via attributes.
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
```v ignore
|
|
|
|
[middleware: check_auth]
|
|
|
|
['/admin/data']
|
|
|
|
pub fn (mut app App) admin() vweb.Result {
|
|
|
|
// ...
|
|
|
|
}
|
|
|
|
|
|
|
|
// check_auth is a method of App, so we don't need to pass the context as parameter.
|
|
|
|
pub fn (mut app App) check_auth () bool {
|
|
|
|
// ...
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
```
|
2023-05-26 03:26:00 +03:00
|
|
|
You can only add 1 middleware to a route specific function via attributes.
|
|
|
|
|
|
|
|
#### Middleware evaluation order
|
|
|
|
The middleware is executed in the following order:
|
|
|
|
|
|
|
|
1. `before_request`
|
|
|
|
2. The middleware in `app.middlewares`
|
|
|
|
3. The middleware in the `[middleware]` attribute
|
|
|
|
|
|
|
|
If any function of step 2 or 3 returns `false` the middleware functions that would
|
|
|
|
come after it are not executed and the app handler will also not be executed. You
|
|
|
|
can think of it as a chain.
|
2023-03-26 02:57:42 +03:00
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
### Redirect
|
|
|
|
|
|
|
|
Used when you want be redirected to an url
|
2023-03-26 02:57:42 +03:00
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
**Examples:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
pub fn (mut app App) before_request() {
|
2023-03-26 02:57:42 +03:00
|
|
|
app.user_id = app.get_cookie('id') or { app.redirect('/') }
|
2022-08-20 12:06:24 +03:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
['/articles'; get]
|
|
|
|
pub fn (mut app App) articles() vweb.Result {
|
2023-03-26 02:57:42 +03:00
|
|
|
if !app.token {
|
|
|
|
app.redirect('/login')
|
|
|
|
}
|
|
|
|
return app.text('patatoes')
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
You can also combine middleware and redirect.
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
[middleware: with_auth]
|
|
|
|
['/admin/secret']
|
|
|
|
pub fn (mut app App) admin_secret() vweb.Result {
|
|
|
|
// this code should never be reached
|
|
|
|
return app.text('secret')
|
|
|
|
}
|
|
|
|
|
|
|
|
['/redirect']
|
|
|
|
pub fn (mut app App) with_auth() bool {
|
|
|
|
app.redirect('/auth/login')
|
|
|
|
return false
|
2022-08-20 12:06:24 +03:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2023-04-12 00:50:03 +03:00
|
|
|
### Fallback route
|
|
|
|
You can implement a fallback `not_found` route that is called when a request is made and no
|
|
|
|
matching route is found.
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
``` v ignore
|
|
|
|
pub fn (mut app App) not_found() vweb.Result {
|
|
|
|
app.set_status(404, 'Not Found')
|
|
|
|
return app.html('<h1>Page not found</h1>')
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2023-04-23 03:37:15 +03:00
|
|
|
### Databases
|
|
|
|
The `db` field in a vweb app is reserved for database connections. The connection is
|
|
|
|
copied to each new request.
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v
|
|
|
|
module main
|
|
|
|
|
|
|
|
import vweb
|
|
|
|
import db.sqlite
|
|
|
|
|
|
|
|
struct App {
|
|
|
|
vweb.Context
|
|
|
|
mut:
|
|
|
|
db sqlite.DB
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
// create the database connection
|
|
|
|
mut db := sqlite.connect('db')!
|
|
|
|
|
|
|
|
vweb.run(&App{
|
|
|
|
db: db
|
|
|
|
}, 8080)
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### Multithreading
|
|
|
|
By default, a vweb app is multithreaded, that means that multiple requests can
|
|
|
|
be handled in parallel by using multiple CPU's: a worker pool. You can
|
|
|
|
change the number of workers (maximum allowed threads) by altering the `nr_workers`
|
|
|
|
option. The default behaviour is to use the maximum number of jobs (cores in most cases).
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
```v ignore
|
|
|
|
fn main() {
|
|
|
|
// assign a maximum of 4 workers
|
|
|
|
vweb.run_at(&App{}, nr_workers: 4)
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### Database Pool
|
|
|
|
A single connection database works fine if you run your app with 1 worker, of if
|
|
|
|
you access a file-based database like a sqlite file.
|
|
|
|
|
|
|
|
This approach will fail when using a non-file based database connection like a mysql
|
|
|
|
connection to another server somewhere on the internet. Multiple threads would need to access
|
|
|
|
the same connection at the same time.
|
|
|
|
|
|
|
|
To resolve this issue, you can use the vweb's built-in database pool. The database pool
|
|
|
|
will keep a number of connections open when the app is started and each worker is
|
|
|
|
assigned its own connection.
|
|
|
|
|
|
|
|
Let's look how we can improve our previous example with database pooling and using a
|
|
|
|
postgresql server instead.
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
```v
|
|
|
|
module main
|
|
|
|
|
|
|
|
import vweb
|
|
|
|
import db.pg
|
|
|
|
|
|
|
|
struct App {
|
|
|
|
vweb.Context
|
|
|
|
db_handle vweb.DatabasePool[pg.DB]
|
|
|
|
mut:
|
|
|
|
db pg.DB
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_database_connection() pg.DB {
|
|
|
|
// insert your own credentials
|
|
|
|
return pg.connect(user: 'user', password: 'password', dbname: 'database') or { panic(err) }
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
// create the database pool and pass our `get_database_connection` function as handler
|
|
|
|
pool := vweb.database_pool(handler: get_database_connection)
|
|
|
|
|
|
|
|
// no need to set the `db` field
|
|
|
|
vweb.run(&App{
|
|
|
|
db_handle: pool
|
|
|
|
}, 8080)
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
If you don't use the default number of workers (`nr_workers`) you have to change
|
|
|
|
it to the same number in `vweb.run_at` as in `vweb.database_pool`
|
|
|
|
|
2023-05-02 22:49:55 +03:00
|
|
|
### Extending the App struct with `[vweb_global]`
|
|
|
|
You can change your `App` struct however you like, but there are some things you
|
|
|
|
have to keep in mind. Under the hood at each request a new instance of `App` is
|
|
|
|
constructed, and all fields are re-initialized with their default type values,
|
|
|
|
except for the `db` field.
|
|
|
|
|
|
|
|
This behaviour ensures that each request is treated equally and in the same context, but
|
|
|
|
problems arise when we want to provide more context than just the default `vweb.Context`.
|
|
|
|
|
|
|
|
Let's view the following example where we want to provide a secret token to our app:
|
|
|
|
|
|
|
|
```v
|
|
|
|
module main
|
|
|
|
|
|
|
|
import vweb
|
|
|
|
|
|
|
|
struct App {
|
|
|
|
vweb.Context
|
|
|
|
secret string
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
vweb.run(&App{
|
|
|
|
secret: 'my secret'
|
|
|
|
}, 8080)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn (mut app App) index() vweb.Result {
|
|
|
|
return app.text('My secret is: ${app.secret}')
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
When you visit `localhost:8080/` you would expect to see the text
|
|
|
|
`"My secret is: my secret"`, but instead there is only the text
|
|
|
|
`"My secret is: "`. This is because of the way vweb works. We can override the default
|
|
|
|
behaviour by adding the attribute `[vweb_global]` to the `secret` field.
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
```v ignore
|
|
|
|
struct App {
|
|
|
|
vweb.Context
|
|
|
|
secret string [vweb_global]
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
Now if you visit `localhost:8080/` you see the text `"My secret is: my secret"`.
|
|
|
|
> **Note**: the value of `secret` gets initialized with the provided value when creating
|
|
|
|
> `App`. If you would modify `secret` in one request the value won't be changed in the
|
|
|
|
> next request. You can use shared fields for this.
|
|
|
|
|
|
|
|
### Shared Objects across requests
|
|
|
|
We saw in the previous section that we can persist data across multiple requests,
|
|
|
|
but what if we want to be able to mutate the data? Since vweb works with threads,
|
|
|
|
we have to use `shared` fields.
|
|
|
|
|
|
|
|
Let's see how we can add a visitor counter to our `App`.
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
```v
|
|
|
|
module main
|
|
|
|
|
|
|
|
import vweb
|
|
|
|
|
|
|
|
struct Counter {
|
|
|
|
pub mut:
|
|
|
|
count int
|
|
|
|
}
|
|
|
|
|
|
|
|
struct App {
|
|
|
|
vweb.Context
|
|
|
|
mut:
|
|
|
|
counter shared Counter // shared fields can only be structs, arrays or maps.
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
// initialize the shared object
|
|
|
|
shared counter := Counter{
|
|
|
|
count: 0
|
|
|
|
}
|
|
|
|
|
|
|
|
vweb.run(&App{
|
|
|
|
counter: counter
|
|
|
|
}, 8080)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn (mut app App) index() vweb.Result {
|
|
|
|
mut count := 0
|
|
|
|
// lock the counter so we can modify it
|
|
|
|
lock app.counter {
|
|
|
|
app.counter.count += 1
|
|
|
|
count = app.counter.count
|
|
|
|
}
|
|
|
|
return app.text('Total visitors: ${count}')
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### Drawback of Shared Objects
|
|
|
|
The drawback of using shared objects is that it affects performance. In the previous example
|
|
|
|
`App.counter` needs to be locked each time the page is loaded if there are simultaneous
|
|
|
|
requests the next requests will have to wait for the lock to be released.
|
|
|
|
|
|
|
|
It is best practice to limit the use of shared objects as much as possible.
|
|
|
|
|
2023-04-02 16:46:43 +03:00
|
|
|
### Controllers
|
|
|
|
Controllers can be used to split up app logic so you are able to have one struct
|
|
|
|
per `"/"`. E.g. a struct `Admin` for urls starting with `"/admin"` and a struct `Foo`
|
|
|
|
for urls starting with `"/foo"`
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
```v
|
|
|
|
module main
|
|
|
|
|
|
|
|
import vweb
|
|
|
|
|
|
|
|
struct App {
|
|
|
|
vweb.Context
|
|
|
|
vweb.Controller
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Admin {
|
|
|
|
vweb.Context
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Foo {
|
|
|
|
vweb.Context
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
mut app := &App{
|
|
|
|
controllers: [
|
|
|
|
vweb.controller('/admin', &Admin{}),
|
|
|
|
vweb.controller('/foo', &Foo{}),
|
|
|
|
]
|
|
|
|
}
|
|
|
|
vweb.run(app, 8080)
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
You can do everything with a controller struct as with a regular `App` struct.
|
|
|
|
The only difference being is that only the main app that is being passed to `vweb.run`
|
|
|
|
is able to have controllers. If you add `vweb.Controller` on a controller struct it
|
|
|
|
will simply be ignored.
|
|
|
|
|
|
|
|
#### Routing
|
|
|
|
Any route inside a controller struct is treated as a relative route to its controller namespace.
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
['/path']
|
|
|
|
pub fn (mut app Admin) path vweb.Result {
|
|
|
|
return app.text('Admin')
|
|
|
|
}
|
|
|
|
```
|
|
|
|
When we created the controller with `vweb.controller('/admin', &Admin{})` we told
|
|
|
|
vweb that the namespace of that controller is `"/admin"` so in this example we would
|
|
|
|
see the text `"Admin"` if we navigate to the url `"/admin/path"`.
|
|
|
|
|
|
|
|
Vweb doesn't support fallback routes or duplicate routes, so if we add the following
|
|
|
|
route to the example the code will produce an error.
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
['/admin/path']
|
|
|
|
pub fn (mut app App) admin_path vweb.Result {
|
|
|
|
return app.text('Admin overwrite')
|
|
|
|
}
|
|
|
|
```
|
|
|
|
There will be an error, because the controller `Admin` handles all routes starting with
|
|
|
|
`"/admin"`; the method `admin_path` is unreachable.
|
|
|
|
|
2023-05-30 15:22:23 +03:00
|
|
|
#### Hosts
|
|
|
|
You can also set a host for a controller. All requests coming from that host will be handled
|
|
|
|
by the controller.
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
```v
|
|
|
|
module main
|
|
|
|
|
|
|
|
import vweb
|
|
|
|
|
|
|
|
struct App {
|
|
|
|
vweb.Context
|
|
|
|
vweb.Controller
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn (mut app App) index() vweb.Result {
|
|
|
|
return app.text('App')
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Example {
|
|
|
|
vweb.Context
|
|
|
|
}
|
|
|
|
|
|
|
|
// You can only access this route at example.com: http://example.com/
|
|
|
|
pub fn (mut app Example) index() vweb.Result {
|
|
|
|
return app.text('Example')
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
vweb.run(&App{
|
|
|
|
controllers: [
|
|
|
|
vweb.controller_host('example.com', '/', &Example{}),
|
|
|
|
]
|
|
|
|
}, 8080)
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2023-04-02 16:46:43 +03:00
|
|
|
#### Databases and `[vweb_global]` in controllers
|
|
|
|
|
2023-04-23 03:37:15 +03:00
|
|
|
Fields with `[vweb_global]` have to passed to each controller individually.
|
|
|
|
The `db` field is unique and will be treated as a `vweb_global` field at all times.
|
2023-04-02 16:46:43 +03:00
|
|
|
|
|
|
|
**Example:**
|
|
|
|
```v
|
|
|
|
module main
|
|
|
|
|
|
|
|
import vweb
|
|
|
|
import db.sqlite
|
|
|
|
|
|
|
|
struct App {
|
|
|
|
vweb.Context
|
|
|
|
vweb.Controller
|
2023-04-23 03:37:15 +03:00
|
|
|
mut:
|
|
|
|
db sqlite.DB
|
2023-04-02 16:46:43 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
struct Admin {
|
|
|
|
vweb.Context
|
2023-04-23 03:37:15 +03:00
|
|
|
mut:
|
|
|
|
db sqlite.DB
|
2023-04-02 16:46:43 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
mut db := sqlite.connect('db')!
|
|
|
|
|
|
|
|
mut app := &App{
|
|
|
|
db: db
|
|
|
|
controllers: [
|
|
|
|
vweb.controller('/admin', &Admin{
|
|
|
|
db: db
|
|
|
|
}),
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2023-04-23 03:37:15 +03:00
|
|
|
#### Using a database pool
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
```v
|
|
|
|
module main
|
|
|
|
|
|
|
|
import vweb
|
|
|
|
import db.pg
|
|
|
|
|
|
|
|
struct App {
|
|
|
|
vweb.Context
|
|
|
|
vweb.Controller
|
|
|
|
db_handle vweb.DatabasePool[pg.DB]
|
|
|
|
mut:
|
|
|
|
db pg.DB
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Admin {
|
|
|
|
vweb.Context
|
|
|
|
db_handle vweb.DatabasePool[pg.DB]
|
|
|
|
mut:
|
|
|
|
db pg.DB
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_database_connection() pg.DB {
|
|
|
|
// insert your own credentials
|
|
|
|
return pg.connect(user: 'user', password: 'password', dbname: 'database') or { panic(err) }
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
// create the database pool and pass our `get_database_connection` function as handler
|
|
|
|
pool := vweb.database_pool(handler: get_database_connection)
|
|
|
|
|
|
|
|
mut app := &App{
|
|
|
|
db_handle: pool
|
|
|
|
controllers: [
|
|
|
|
vweb.controller('/admin', &Admin{
|
|
|
|
db_handle: pool
|
|
|
|
}),
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
### Responses
|
|
|
|
|
|
|
|
#### - set_status
|
|
|
|
|
|
|
|
Sets the response status
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
['/user/get_all'; get]
|
|
|
|
pub fn (mut app App) controller_get_all_user() vweb.Result {
|
|
|
|
token := app.get_header('token')
|
|
|
|
|
|
|
|
if !token {
|
|
|
|
app.set_status(401, '')
|
|
|
|
return app.text('Not valid token')
|
|
|
|
}
|
|
|
|
|
|
|
|
response := app.service_get_all_user() or {
|
|
|
|
app.set_status(400, '')
|
|
|
|
return app.text('$err')
|
|
|
|
}
|
|
|
|
return app.json(response)
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### - html
|
|
|
|
|
|
|
|
Response HTTP_OK with payload with content-type `text/html`
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
pub fn (mut app App) html_page() vweb.Result {
|
|
|
|
return app.html('<h1>ok</h1>')
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### - text
|
|
|
|
|
|
|
|
Response HTTP_OK with payload with content-type `text/plain`
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
pub fn (mut app App) simple() vweb.Result {
|
|
|
|
return app.text('A simple result')
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### - json
|
|
|
|
|
|
|
|
Response HTTP_OK with payload with content-type `application/json`
|
|
|
|
**Examples:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
['/articles'; get]
|
|
|
|
pub fn (mut app App) articles() vweb.Result {
|
|
|
|
articles := app.find_all_articles()
|
|
|
|
json_result := json.encode(articles)
|
|
|
|
return app.json(json_result)
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
['/user/create'; post]
|
|
|
|
pub fn (mut app App) controller_create_user() vweb.Result {
|
|
|
|
body := json.decode(User, app.req.data) or {
|
|
|
|
app.set_status(400, '')
|
|
|
|
return app.text('Failed to decode json, error: $err')
|
|
|
|
}
|
|
|
|
|
|
|
|
response := app.service_add_user(body.username, body.password) or {
|
|
|
|
app.set_status(400, '')
|
|
|
|
return app.text('error: $err')
|
|
|
|
}
|
|
|
|
|
|
|
|
return app.json(response)
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### - json_pretty
|
|
|
|
|
|
|
|
Response HTTP_OK with a pretty-printed JSON result
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
fn (mut app App) time_json_pretty() {
|
|
|
|
app.json_pretty({
|
|
|
|
'time': time.now().format()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### - file
|
|
|
|
|
|
|
|
Response HTTP_OK with file as payload
|
|
|
|
|
|
|
|
#### - ok
|
|
|
|
|
|
|
|
Response HTTP_OK with payload
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
['/form_echo'; post]
|
|
|
|
pub fn (mut app App) form_echo() vweb.Result {
|
|
|
|
app.set_content_type(app.req.header.get(.content_type) or { '' })
|
|
|
|
return app.ok(app.form['foo'])
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### - server_error
|
|
|
|
|
|
|
|
Response a server error
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
fn (mut app App) sse() vweb.Result {
|
|
|
|
return app.server_error(501)
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### - not_found
|
|
|
|
|
|
|
|
Response HTTP_NOT_FOUND with payload
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
['/:user/:repo/settings']
|
|
|
|
pub fn (mut app App) user_repo_settings(username string, repository string) vweb.Result {
|
|
|
|
if username !in known_users {
|
|
|
|
return app.not_found()
|
|
|
|
}
|
|
|
|
return app.html('username: $username | repository: $repository')
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### Requests
|
|
|
|
|
|
|
|
#### - get_header
|
|
|
|
|
|
|
|
Returns the header data from the key
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
['/user/get_all'; get]
|
|
|
|
pub fn (mut app App) controller_get_all_user() vweb.Result {
|
|
|
|
token := app.get_header('token')
|
|
|
|
return app.text(token)
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### - get_cookie
|
|
|
|
|
|
|
|
Sets a cookie
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
pub fn (mut app App) before_request() {
|
|
|
|
app.user_id = app.get_cookie('id') or { '0' }
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### - add_header
|
|
|
|
|
|
|
|
Adds an header to the response with key and val
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
['/upload'; post]
|
|
|
|
pub fn (mut app App) upload() vweb.Result {
|
|
|
|
fdata := app.files['upfile']
|
|
|
|
|
|
|
|
data_rows := fdata[0].data.split('\n')
|
|
|
|
|
|
|
|
mut output_data := ''
|
|
|
|
|
|
|
|
for elem in data_rows {
|
|
|
|
delim_row := elem.split('\t')
|
|
|
|
output_data += '${delim_row[0]}\t${delim_row[1]}\t'
|
|
|
|
output_data += '${delim_row[0].int() + delim_row[1].int()}\n'
|
|
|
|
}
|
|
|
|
|
|
|
|
output_data = output_data.all_before_last('\n')
|
|
|
|
|
|
|
|
app.add_header('Content-Disposition', 'attachment; filename=results.txt')
|
|
|
|
app.send_response_to_client('application/octet-stream', output_data)
|
|
|
|
|
|
|
|
return $vweb.html()
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### - set_cookie
|
|
|
|
|
|
|
|
Sets a cookie
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
pub fn (mut app App) cookie() vweb.Result {
|
|
|
|
app.set_cookie(name: 'cookie', value: 'test')
|
|
|
|
return app.text('Response Headers\n$app.header')
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### - set_cookie_with_expire_date
|
|
|
|
|
|
|
|
Sets a cookie with a `expire_data`
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
pub fn (mut app App) cookie() vweb.Result {
|
|
|
|
key := 'cookie'
|
|
|
|
value := 'test'
|
|
|
|
duration := time.Duration(2 * time.minute ) // add 2 minutes
|
|
|
|
expire_date := time.now().add(duration)
|
|
|
|
|
|
|
|
app.set_cookie_with_expire_date(key, value, expire_date)
|
|
|
|
return app.text('Response Headers\n$app.header')
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### - set_content_type
|
|
|
|
|
|
|
|
Sets the response content type
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
['/form_echo'; post]
|
|
|
|
pub fn (mut app App) form_echo() vweb.Result {
|
|
|
|
app.set_content_type(app.req.header.get(.content_type) or { '' })
|
|
|
|
return app.ok(app.form['foo'])
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### Template
|
|
|
|
|
|
|
|
#### -handle_static
|
|
|
|
|
|
|
|
handle_static is used to mark a folder (relative to the current working folder) as one that
|
2023-06-02 10:55:59 +03:00
|
|
|
contains only static resources (css files, images etc).\
|
|
|
|
host_handle_static can be used to limit the static resources to a specific host.
|
2022-08-20 12:06:24 +03:00
|
|
|
|
|
|
|
If `root` is set the mount path for the dir will be in '/'
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
fn main() {
|
|
|
|
mut app := &App{}
|
|
|
|
app.serve_static('/favicon.ico', 'favicon.ico')
|
2023-06-02 10:55:59 +03:00
|
|
|
// app.host_serve_static('localhost', '/favicon.ico', 'favicon.ico')
|
2022-08-20 12:06:24 +03:00
|
|
|
// Automatically make available known static mime types found in given directory.
|
|
|
|
os.chdir(os.dir(os.executable()))?
|
|
|
|
app.handle_static('assets', true)
|
|
|
|
vweb.run(app, port)
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### -mount_static_folder_at
|
|
|
|
|
|
|
|
makes all static files in `directory_path` and inside it, available at http://server/mount_path.
|
|
|
|
|
|
|
|
For example: suppose you have called .mount_static_folder_at('/var/share/myassets', '/assets'),
|
|
|
|
and you have a file /var/share/myassets/main.css .
|
|
|
|
=> That file will be available at URL: http://server/assets/main.css .
|
|
|
|
|
2023-06-02 10:55:59 +03:00
|
|
|
mount_static_folder_at can be used to limit the static resources to a specific host.
|
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
#### -serve_static
|
|
|
|
|
|
|
|
Serves a file static.
|
|
|
|
`url` is the access path on the site, `file_path` is the real path to the file, `mime_type` is the
|
|
|
|
file type
|
|
|
|
|
2023-06-02 10:55:59 +03:00
|
|
|
host_serve_static can be used to limit the static resources to a specific host.
|
|
|
|
|
2022-08-20 12:06:24 +03:00
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
fn main() {
|
|
|
|
mut app := &App{}
|
|
|
|
app.serve_static('/favicon.ico', 'favicon.ico')
|
2023-06-02 10:55:59 +03:00
|
|
|
// app.host_serve_static('localhost', /favicon.ico', 'favicon.ico')
|
2022-08-20 12:06:24 +03:00
|
|
|
app.mount_static_folder_at(os.resource_abs_path('.'), '/')
|
|
|
|
vweb.run(app, 8081)
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### Others
|
|
|
|
|
|
|
|
#### -ip
|
|
|
|
|
|
|
|
Returns the ip address from the current user
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
pub fn (mut app App) ip() vweb.Result {
|
|
|
|
ip := app.ip()
|
|
|
|
return app.text('ip: $ip')
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### -error
|
|
|
|
|
|
|
|
Set a string to the form error
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
|
|
|
|
```v ignore
|
|
|
|
pub fn (mut app App) error() vweb.Result {
|
|
|
|
app.error('here as an error')
|
|
|
|
println(app.form_error) //'vweb error: here as an error'
|
|
|
|
}
|
|
|
|
```
|
2022-10-29 06:33:48 +03:00
|
|
|
# Cross-Site Request Forgery (CSRF) protection
|
|
|
|
|
2023-05-21 01:56:17 +03:00
|
|
|
Vweb has built-in csrf protection. Go to the [csrf module](csrf/) to learn how
|
|
|
|
you can protect your app against CSRF.
|