Skip to content

Commit

Permalink
examples: fullstack vweb example (#16761)
Browse files Browse the repository at this point in the history
  • Loading branch information
enghitalo committed Jan 6, 2023
1 parent 43d8bc3 commit 0146509
Show file tree
Hide file tree
Showing 27 changed files with 759 additions and 0 deletions.
1 change: 1 addition & 0 deletions cmd/tools/vbuild-examples.v
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const vroot = @VMODROOT
const efolders = [
'examples/viewer',
'examples/vweb_orm_jwt',
'examples/vweb_fullstack',
]

fn main() {
Expand Down
9 changes: 9 additions & 0 deletions examples/vweb_fullstack/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.v]
indent_style = tab
indent_size = 4
7 changes: 7 additions & 0 deletions examples/vweb_fullstack/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
* text=auto eol=lf
*.bat eol=crlf

**/*.v linguist-language=V
**/*.vv linguist-language=V
**/*.vsh linguist-language=V
**/v.mod linguist-language=V
18 changes: 18 additions & 0 deletions examples/vweb_fullstack/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Binaries for programs and plugins
main
batata
*.exe
*.exe~
*.so
*.dylib
*.dll
*.sql

# Ignore binary output folders
bin/

# Ignore common editor/system specific metadata
.DS_Store
.idea/
.vscode/
*.iml
21 changes: 21 additions & 0 deletions examples/vweb_fullstack/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 Hitalo Souza

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Binary file added examples/vweb_fullstack/src/assets/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions examples/vweb_fullstack/src/assets/v-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/vweb_fullstack/src/assets/veasel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions examples/vweb_fullstack/src/auth_controllers.v
Original file line number Diff line number Diff line change
@@ -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)
}
6 changes: 6 additions & 0 deletions examples/vweb_fullstack/src/auth_dto.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module main

struct AuthRequestDto {
username string [required]
password string [required]
}
90 changes: 90 additions & 0 deletions examples/vweb_fullstack/src/auth_services.v
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
74 changes: 74 additions & 0 deletions examples/vweb_fullstack/src/index.html
Original file line number Diff line number Diff line change
@@ -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>
40 changes: 40 additions & 0 deletions examples/vweb_fullstack/src/main.v
Original file line number Diff line number Diff line change
@@ -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()
}
62 changes: 62 additions & 0 deletions examples/vweb_fullstack/src/product_controller.v
Original file line number Diff line number Diff line change
@@ -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')
}
9 changes: 9 additions & 0 deletions examples/vweb_fullstack/src/product_entities.v
Original file line number Diff line number Diff line change
@@ -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']
}
Loading

0 comments on commit 0146509

Please sign in to comment.