1
0
mirror of https://github.com/vlang/v.git synced 2023-08-10 21:13:21 +03:00

examples: fullstack vweb example (#16761)

This commit is contained in:
Hitalo Souza
2023-01-05 22:36:42 -03:00
committed by GitHub
parent 43d8bc30f9
commit 0146509516
27 changed files with 759 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 500 500" width="500px" height="500px"><defs><clipPath id="_clipPath_8TWIgR1z3pxinjWBiigzcEIrVJKv9Gq4"><rect width="500" height="500"/></clipPath></defs><g clip-path="url(#_clipPath_8TWIgR1z3pxinjWBiigzcEIrVJKv9Gq4)"><path d=" M 318.422 453.543 L 463.705 49.541 C 466.168 42.689 462.285 37.693 455.037 38.392 L 340.786 49.398 C 333.539 50.097 325.71 56.246 323.316 63.121 L 188.843 449.216 C 186.447 456.091 190.414 461.673 197.695 461.673 L 308.901 461.673 C 312.541 461.673 316.497 458.893 317.729 455.466 L 318.422 453.543 Z " fill="rgb(83,107,138)"/><defs><filter id="Hmac7mZraFWHw0G84Yxj4QuzeTFp0E7Y" x="-200%" y="-200%" width="400%" height="400%" filterUnits="objectBoundingBox" color-interpolation-filters="sRGB"><feGaussianBlur xmlns="http://www.w3.org/2000/svg" in="SourceGraphic" stdDeviation="6.440413594258542"/><feOffset xmlns="http://www.w3.org/2000/svg" dx="0" dy="0" result="pf_100_offsetBlur"/><feFlood xmlns="http://www.w3.org/2000/svg" flood-color="#000000" flood-opacity="0.65"/><feComposite xmlns="http://www.w3.org/2000/svg" in2="pf_100_offsetBlur" operator="in" result="pf_100_dropShadow"/><feBlend xmlns="http://www.w3.org/2000/svg" in="SourceGraphic" in2="pf_100_dropShadow" mode="normal"/></filter></defs><g filter="url(#Hmac7mZraFWHw0G84Yxj4QuzeTFp0E7Y)"><path d=" M 301.848 455.466 L 241.359 280.725 L 250 275.324 L 311.57 453.543 L 301.848 455.466 Z " fill="rgb(235,235,235)"/></g><path d=" M 44.963 38.392 L 159.214 49.398 C 166.461 50.097 174.298 56.243 176.704 63.115 L 314.022 455.448 C 315.224 458.885 313.245 461.673 309.604 461.673 L 197.695 461.673 C 190.414 461.673 182.502 456.111 180.038 449.259 L 36.295 49.541 C 33.832 42.689 37.715 37.693 44.963 38.392 Z " fill="rgb(93,135,191)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

View File

@ -0,0 +1,13 @@
module main
import vweb
['/controller/auth'; post]
pub fn (mut app App) controller_auth(username string, password string) vweb.Result {
response := app.service_auth(username, password) or {
app.set_status(400, '')
return app.text('error: ${err}')
}
return app.json(response)
}

View File

@ -0,0 +1,6 @@
module main
struct AuthRequestDto {
username string [required]
password string [required]
}

View File

@ -0,0 +1,90 @@
module main
import crypto.hmac
import crypto.sha256
import crypto.bcrypt
import encoding.base64
import json
import databases
import time
struct JwtHeader {
alg string
typ string
}
struct JwtPayload {
sub string // (subject) = Entity to whom the token belongs, usually the user ID;
iss string // (issuer) = Token issuer;
exp string // (expiration) = Timestamp of when the token will expire;
iat time.Time // (issued at) = Timestamp of when the token was created;
aud string // (audience) = Token recipient, represents the application that will use it.
name string
roles string
permissions string
}
fn (mut app App) service_auth(username string, password string) !string {
mut db := databases.create_db_connection() or {
eprintln(err)
panic(err)
}
defer {
db.close() or { panic('fail to close database') }
}
user := sql db {
select from User where username == username limit 1
}
if user.username != username {
return error('user not found')
}
if !user.active {
return error('user is not active')
}
bcrypt.compare_hash_and_password(password.bytes(), user.password.bytes()) or {
return error('Failed to auth user, ${err}')
}
token := make_token(user)
return token
}
fn make_token(user User) string {
secret := 'SECRET_KEY' // os.getenv('SECRET_KEY')
jwt_header := JwtHeader{'HS256', 'JWT'}
jwt_payload := JwtPayload{
sub: '${user.id}'
name: '${user.username}'
iat: time.now()
}
header := base64.url_encode(json.encode(jwt_header).bytes())
payload := base64.url_encode(json.encode(jwt_payload).bytes())
signature := base64.url_encode(hmac.new(secret.bytes(), '${header}.${payload}'.bytes(),
sha256.sum, sha256.block_size).bytestr().bytes())
jwt := '${header}.${payload}.${signature}'
return jwt
}
fn auth_verify(token string) bool {
if token == '' {
return false
}
secret := 'SECRET_KEY' // os.getenv('SECRET_KEY')
token_split := token.split('.')
signature_mirror := hmac.new(secret.bytes(), '${token_split[0]}.${token_split[1]}'.bytes(),
sha256.sum, sha256.block_size).bytestr().bytes()
signature_from_token := base64.url_decode(token_split[2])
return hmac.equal(signature_from_token, signature_mirror)
// return true
}

View File

@ -0,0 +1,8 @@
module databases
import sqlite // can change to 'mysql', 'pg'
pub fn create_db_connection() !sqlite.DB {
mut db := sqlite.connect('vweb.sql')!
return db
}

View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<head>
<!--Let browser know website is optimized for mobile-->
<meta charset="UTF-8" name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Compiled and minified CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<!-- Compiled and minified JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<!-- Material UI icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<title>${title}</title>
</head>
<body>
<div>@include 'templates/header_component.html'</div>
<div class="card-panel center-align" style="max-width: 240px; padding: 10px; margin: 10px; border-radius: 5px;">
<form id="index_form" method='post' action=''>
<div style="display:flex; flex-direction: column;">
<input type='text' name='username' placeholder='Username' required autofocus>
<input type='password' name='password' placeholder='Password' required>
</div>
<div style="margin-top: 10px;">
<input class="waves-effect waves-light btn-small" type='submit' onclick="login()" formaction="javascript:void(0);" value='Login'>
<input class="waves-effect waves-light btn-small" type='submit' onclick="addUser()" formaction="javascript:void(0);" value='Register'>
</div>
</form>
<script type="text/javascript">
// function eraseCookie(name) {
// document.cookie = name + '=; Max-Age=0'
// }
async function addUser() {
const form = document.querySelector('#index_form');
const formData = new FormData(form);
await fetch('/controller/user/create', {
method: 'POST',
body: formData
})
.then( async (response) => {
if (response.status != 201) {
throw await response.text()
}
return await response.text()
})
.then((data) => {
alert("User created successfully")
})
.catch((error) => {
alert(error);
});
}
async function login() {
const form = document.querySelector('#index_form');
const formData = new FormData(form);
await fetch('/controller/auth', {
method: 'POST',
body: formData
})
.then( async (response) => {
if (response.status != 200) {
throw await response.text()
}
return response.json()
})
.then((data) => {
document.cookie = 'token='+data+';';
window.location.href = '/products'
})
.catch((error) => {
alert(error);
});
}
</script>
</div>
</body>
</html>

View File

@ -0,0 +1,40 @@
module main
import vweb
import databases
import os
const (
port = 8082
)
struct App {
vweb.Context
}
pub fn (app App) before_request() {
println('[web] before_request: ${app.req.method} ${app.req.url}')
}
fn main() {
mut db := databases.create_db_connection() or { panic(err) }
sql db {
create table User
} or { panic('error on create table: ${err}') }
db.close() or { panic(err) }
mut app := &App{}
app.serve_static('/favicon.ico', 'src/assets/favicon.ico')
// makes all static files available.
app.mount_static_folder_at(os.resource_abs_path('.'), '/')
vweb.run(app, port)
}
pub fn (mut app App) index() vweb.Result {
title := 'vweb app'
return $vweb.html()
}

View File

@ -0,0 +1,62 @@
module main
import vweb
import encoding.base64
import json
['/controller/products'; get]
pub fn (mut app App) controller_get_all_products() vweb.Result {
token := app.req.header.get_custom('token') or { '' }
if !auth_verify(token) {
app.set_status(401, '')
return app.text('Not valid token')
}
jwt_payload_stringify := base64.url_decode_str(token.split('.')[1])
jwt_payload := json.decode(JwtPayload, jwt_payload_stringify) or {
app.set_status(501, '')
return app.text('jwt decode error')
}
user_id := jwt_payload.sub
response := app.service_get_all_products_from(user_id.int()) or {
app.set_status(400, '')
return app.text('${err}')
}
return app.json(response)
// return app.text('response')
}
['/controller/product/create'; post]
pub fn (mut app App) controller_create_product(product_name string) vweb.Result {
if product_name == '' {
app.set_status(400, '')
return app.text('product name cannot be empty')
}
token := app.req.header.get_custom('token') or { '' }
if !auth_verify(token) {
app.set_status(401, '')
return app.text('Not valid token')
}
jwt_payload_stringify := base64.url_decode_str(token.split('.')[1])
jwt_payload := json.decode(JwtPayload, jwt_payload_stringify) or {
app.set_status(501, '')
return app.text('jwt decode error')
}
user_id := jwt_payload.sub
app.service_add_product(product_name, user_id.int()) or {
app.set_status(400, '')
return app.text('error: ${err}')
}
app.set_status(201, '')
return app.text('product created successfully')
}

View File

@ -0,0 +1,9 @@
module main
[table: 'products']
struct Product {
id int [primary; sql: serial]
user_id int
name string [required; sql_type: 'TEXT']
created_at string [default: 'CURRENT_TIMESTAMP']
}

View File

@ -0,0 +1,43 @@
module main
import databases
fn (mut app App) service_add_product(product_name string, user_id int) ! {
mut db := databases.create_db_connection()!
defer {
db.close() or { panic(err) }
}
product_model := Product{
name: product_name
user_id: user_id
}
mut insert_error := ''
sql db {
insert product_model into Product
} or { insert_error = err.msg() }
if insert_error != '' {
return error(insert_error)
}
}
fn (mut app App) service_get_all_products_from(user_id int) ?[]Product {
mut db := databases.create_db_connection() or {
println(err)
return err
}
defer {
db.close() or { panic(err) }
}
results := sql db {
select from Product where user_id == user_id
}
return results
}

View File

@ -0,0 +1,18 @@
module main
import vweb
['/products'; get]
pub fn (mut app App) products() !vweb.Result {
token := app.get_cookie('token') or {
app.set_status(400, '')
return app.text('${err}')
}
user := get_user(token) or {
app.set_status(400, '')
return app.text('Failed to fetch data from the server. Error: ${err}')
}
return $vweb.html()
}

View File

@ -0,0 +1,35 @@
module main
import json
import net.http
pub fn get_products(token string) ![]Product {
mut header := http.new_header()
header.add_custom('token', token)!
url := 'http://localhost:8082/controller/products'
mut config := http.FetchConfig{
header: header
}
resp := http.fetch(http.FetchConfig{ ...config, url: url })!
products := json.decode([]Product, resp.body)!
return products
}
pub fn get_product(token string) ![]User {
mut header := http.new_header()
header.add_custom('token', token)!
url := 'http://localhost:8082/controller/product'
mut config := http.FetchConfig{
header: header
}
resp := http.fetch(http.FetchConfig{ ...config, url: url })!
products := json.decode([]User, resp.body)!
return products
}

View File

@ -0,0 +1,15 @@
<nav>
<div class="nav-wrapper">
<a href="javascript:window.history.back();" class="left">
<i class="material-icons">arrow_back_ios_new</i>
</a>
<a href="/">
<img src="src/assets/veasel.png" alt="logo" style="max-height: 100%" />
</a>
<ul id="nav-mobile" class="right">
<li><a href="https://github.com/vlang/v">github</a></li>
<li><a href="https://vlang.io/">website</a></li>
<li><a href="https://github.com/sponsors/medvednikov">support</a></li>
</ul>
</div>
</nav>

View File

@ -0,0 +1,11 @@
h1.title {
font-family: Arial, Helvetica, sans-serif;
color: #3b7bbf;
}
div.products-table {
border: 1px solid;
max-width: 720px;
padding: 10px;
margin: 10px;
}

View File

@ -0,0 +1,93 @@
<!DOCTYPE html>
<head>
<!--Let browser know website is optimized for mobile-->
<meta charset="UTF-8" name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Compiled and minified CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<!-- Compiled and minified JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<!-- Material UI icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<title>Login</title>
@css 'src/templates/products.css'
</head>
<body>
<div>@include 'header_component.html'</div>
<h1 class="title">Hi, ${user.username}! you are online</h1>
<!-- <button onclick="document.location.reload(true)">Lala</button> -->
<form id="product_form" method='post' action=''>
<div class="row">
<div class="input-field col s2">
<input id="product_name" name='product_name' type="text" class="validate">
<label class="active" for="product_name">product name</label>
</div>
<div style="margin-top: 10px;">
<input class="waves-effect waves-light btn-small" type='submit' onclick="addProduct()" formaction="javascript:void(0);" value='Register' required autofocus>
</div>
</div>
<!-- <div style="width: 20; height: 300;">
<input type='text' name='product_name' placeholder='product name' required autofocus>
</div> -->
</form>
<script type="text/javascript">
function getCookie(cookieName) {
let cookie = {};
document.cookie.split(';').forEach(function(el) {
let [key,value] = el.split('=');
cookie[key.trim()] = value;
})
return cookie[cookieName];
}
async function addProduct() {
const form = document.querySelector('#product_form');
const formData = new FormData(form);
console.log(getCookie("token"));
await fetch('/controller/product/create', {
method: 'POST',
body: formData,
headers :{
token: getCookie("token")
}
})
.then( async (response) => {
if (response.status != 201) {
throw await response.text()
}
return await response.text()
})
.then((data) => {
// alert("User created successfully")
document.location.reload(true)
})
.catch((error) => {
alert(error);
});
}
</script>
<div class="products-table card-panel">
<table class="highlight striped responsive-table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Created date</th>
</tr>
</thead>
<tbody>
@for product in user.products
<tr>
<td>${product.id}</td>
<td>${product.name}</td>
<td>${product.created_at}</td>
</tr>
@end
</tbody>
</table>
</div>
</body>
</html>

View File

@ -0,0 +1,66 @@
module main
import vweb
import encoding.base64
import json
['/controller/users'; get]
pub fn (mut app App) controller_get_all_user() vweb.Result {
// token := app.get_cookie('token') or { '' }
token := app.req.header.get_custom('token') or { '' }
if !auth_verify(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)
}
['/controller/user'; get]
pub fn (mut app App) controller_get_user() vweb.Result {
// token := app.get_cookie('token') or { '' }
token := app.req.header.get_custom('token') or { '' }
if !auth_verify(token) {
app.set_status(401, '')
return app.text('Not valid token')
}
jwt_payload_stringify := base64.url_decode_str(token.split('.')[1])
jwt_payload := json.decode(JwtPayload, jwt_payload_stringify) or {
app.set_status(501, '')
return app.text('jwt decode error')
}
user_id := jwt_payload.sub
response := app.service_get_user(user_id.int()) or {
app.set_status(400, '')
return app.text('${err}')
}
return app.json(response)
}
['/controller/user/create'; post]
pub fn (mut app App) controller_create_user(username string, password string) vweb.Result {
if username == '' {
app.set_status(400, '')
return app.text('username cannot be empty')
}
if password == '' {
app.set_status(400, '')
return app.text('password cannot be empty')
}
app.service_add_user(username, password) or {
app.set_status(400, '')
return app.text('error: ${err}')
}
app.set_status(201, '')
return app.text('User created successfully')
}

View File

@ -0,0 +1,11 @@
module main
[table: 'users']
pub struct User {
mut:
id int [primary; sql: serial]
username string [required; sql_type: 'TEXT'; unique]
password string [required; sql_type: 'TEXT']
active bool
products []Product [fkey: 'user_id']
}

View File

@ -0,0 +1,65 @@
module main
import crypto.bcrypt
import databases
fn (mut app App) service_add_user(username string, password string) ! {
mut db := databases.create_db_connection()!
defer {
db.close() or { panic(err) }
}
hashed_password := bcrypt.generate_from_password(password.bytes(), bcrypt.min_cost) or {
eprintln(err)
return err
}
user_model := User{
username: username
password: hashed_password
active: true
}
mut insert_error := ''
sql db {
insert user_model into User
} or { insert_error = err.msg() }
if insert_error != '' {
return error(insert_error)
}
}
fn (mut app App) service_get_all_user() ?[]User {
mut db := databases.create_db_connection() or {
println(err)
return err
}
defer {
db.close() or { panic(err) }
}
results := sql db {
select from User
}
return results
}
fn (mut app App) service_get_user(id int) ?User {
mut db := databases.create_db_connection() or {
println(err)
return err
}
defer {
db.close() or { panic(err) }
}
results := sql db {
select from User where id == id
}
return results
}

View File

@ -0,0 +1,36 @@
module main
import json
import net.http
pub fn get_users(token string) ![]User {
mut header := http.new_header()
header.add_custom('token', token)!
url := 'http://localhost:8082/controller/users'
mut config := http.FetchConfig{
header: header
}
resp := http.fetch(http.FetchConfig{ ...config, url: url })!
users := json.decode([]User, resp.body)!
return users
}
pub fn get_user(token string) !User {
mut header := http.new_header()
header.add_custom('token', token)!
url := 'http://localhost:8082/controller/user'
mut config := http.FetchConfig{
header: header
}
resp := http.fetch(http.FetchConfig{ ...config, url: url })!
users := json.decode(User, resp.body)!
return users
}

View File

@ -0,0 +1,7 @@
Module {
name: 'vweb_fullstack'
description: ''
version: ''
license: ''
dependencies: []
}