Atila Extension For VueJS 2 SFC and SSR
Project description
Introduction
Atla-Vue is Atila extension package for using vue3-sfc-loader and Bootstrap 5.
It will be useful for building simple web service at situation frontend developer dose not exists.
Due to the vue3-sfc-loader, We can use vue single file component on the fly without any compiling or building process.
Atila-Vue composes these things:
- VueJS 3
- VueRouter 4
- Vuex 4
- Optional Bootstrap 5 for UI/UX
For injecting objects to Vuex, it uses Jinja2 template engine.
Full Example
See atila-vue repository and atila-vue examplet.
Launching Server
mkdir myservice
cd myservice
skitaid.py
#! /usr/bin/env python3
import skitai
import atila_vue
from atila import Allied
import backend
os.environ ['SECRET_KEY'] = 'SECRET_KEY'
if __name__ == '__main__':
with skitai.preference () as pref:
skitai.mount ('/', Allied (atila_vue, backend), pref)
skitai.run (ip = '0.0.0.0', name = 'myservice')
backend/__init__.py
import skitai
def __config__ (pref):
pref.set_static ('/', skitai.joinpath ('backend/static'))
pref.config.MAX_UPLOAD_SIZE = 1 * 1024 * 1024 * 1024
pref.config.FRONTEND = {
"googleAnalytics": {"id": "UA-158163406-1"}
}
def __app__ ():
import atila
return atila.Atila (__name__)
def __mount__ (context, app):
import atila_vue
@app.route ("/api")
def api (context):
return {'version': atila_vue.__version__}
@app.route ("/ping")
def ping (context):
return 'pong'
Now you can startup service.
./serve/py --devel
Then your browser address bar, enter http://localhost:5000/ping
.
Again, backend/__init__.py
def __mount__ (context, app):
import atila_vue
@app.route ('/')
@app.route ('/mpa')
def mpa (context):
return context.render (
'mpa.j2',
version = atila_vue.__version__,
use_vue_loader = False
)
@app.route ('/spa/<path:path>')
def spa (context, path = None):
return context.render (
'spa.j2',
route_base = context.baseurl (spa),
version = atila_vue.__version__
)
Templates
Site Template
backend/templates/site.j2
{% extends 'atila-vue/bs5.j2' %}
{% block lang %}en{% endblock %}
{% block state_map %}
{{ set_cloak (False) }}
{% endblock %}
Multi Page App Template
backend/templates/mpa.j2
{% extends 'site.j2' %}
{% block content %}
{% include 'includes/header.j2' %}
<div class="container">
<h1>Multi Page App</h1>
</div>
{% endblock content %}
Single Page App Template
backend/templates/spa.j2
{% extends 'site.j2' %}
{% block content %}
{% include 'includes/header.j2' %}
{{ super () }}
{% endblock %}
Using Vue Router
As creating vue files, vue-router will be automatically configured.
backend/static/apps/spa/index.vue
: /spabackend/static/apps/spa/sub.vue
: /spa/subbackend/static/apps/spa/items/index.vue
: /spa/itemsbackend/static/apps/spa/items/_id.vue
: /spa/items/:id
App Layout
backend/static/apps/spa/layout.vue
<template>
<router-view v-slot="{ Component }">
<transition>
<keep-alive>
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</template>
<script>
export default {}
</script>
Optional Component To Use In Pages
backend/static/apps/spa/components/myComponent.vue
<template>
My Component
</template>
<script>
export default {}
</script>
Route Pages
backend/static/apps/spa/index.vue
<template>
<div class="container">
<h1>Main Page</h1>
<span class="example">
<i class="bi-alarm"></i>{{ msg }}</span>
<div><router-link :to="{ name: 'sub'}">Sub Page</router-link></div>
<div><router-link :to="{ name: 'items'}">Items</router-link></div>
<div><my-component></my-component></div>
</div>
</template>
<script>
import myComponent from '/apps/spa/components/myComponent.vue'
export default {
setup (props, context) {
const msg = ref ('hello world!')
return { msg }
},
components: {
'my-component': myComponent
},
}
</script>
backend/static/apps/spa/sub.vue
<template>
<div class="container">
<h1>Sub Page</h1>
<div><router-link :to="{ name: 'index'}">Main Page</router-link></div>
</div>
</template>
<script>
export default {}
</script>
backend/static/apps/spa/items/index.vue
<template>
<div class="container">
<h1>Items</h1>
<ul>
<li v-for="index in 100" :key="index">
<router-link :to="{ name: 'items/:id', params: {id: index}}">Item {{ index }}</router-link>
</li>
</ul>
</div>
</template>
<script>
export default {}
</script>
backend/static/apps/spa/items/_id_.vue
<template>
<div class="container">
<h1 class='ko-b'>Item {{ item_id }}</h1>
</div>
</template>
<style scoped>
.example {
color: v-bind('color');
}
</style>
<script>
export default {
setup (props, context) {
const route = useRoute ()
const item_id = ref (route.params.id)
onActivated (() => {
item_id.value = route.params.id
})
watch (() => item_id.value,
(to, from) => {
log (`item changed: ${from} => ${to}`)
}
)
return { item_id }
},
beforeRouteEnter (to, from, next) {
next ()
}
}
</script>
Using Vuex
You can define Vuex state.
Update backend/templates/main.j2
.
{% extends '__framework/bs5.j2' %}
{{ map_state ('page_id', 0) }}
{{ map_state ('types', ["todo", "canceled", "done"]) }}
These will be injected to Vuex
through JSON.
Now tou can use these state on your vue file with useStore
.
<script>
import {ref, computed, useStore} from '/vue/composition-api.js'
export default {
setup () {
const store = useStore ()
const page_id = computed ( () => store.state.page_id )
const msg = ref ('Hello World')
return { msg, page_id }
}
}
</script>
Or use useState
.
<script>
import {ref, useState} from '/vue/composition-api.js'
export default {
setup () {
const { page_id } = useState ()
const msg = ref ('Hello World')
return { msg, page_id }
}
}
</script>
Note that /vue/composition-api.js contains some shortcuts for Vue.
, Vuex.
and VueRouter
.
Adding APIs
mkdir backend/services
Create backend/services/apis.py
def __mount__ (app. mntopt):
@app.route ("")
def index (was):
return "API Index"
@app.route ("/now")
def now (was):
return was.API (result = time.time ())
Create backend/services/__init__.py
def __setup__ (app. mntopt):
from . import apis
app.mount ('/apis', apis)
Then update backend/__init__.py
for mount services
.
def __app__ ():
return atila.Atila (__name__)
def __setup__ (app, mntopt):
from . import services
app.mount ('/', services)
def __mount__ (app, mntopt):
@app.route ('/')
def index (was):
return was.render ('main.j2')
Now you can use API: http://localhost:5000/apis/now.
<script>
import {ref, onBeforeMount} from '/vue/composition-api.js'
import {$http} from '/veu/helpers.js'
export default {
setup () {
const msg = ref ('Hello World')
const server_time = ref (null)
onBeforeMount ( () => {
const r = await $http.get ('/apis/now')
server_time.value = r.data.result
})
return { msg, server_time }
}
}
</script>
Note that $http
is the alias for axios
.
Accessing APIs
Vuex.state has $apispecs
state and it contains all API specification of server side. We made only 1 APIs for now.
Note that your exposed APIs endpoint should be /api
.
{
APIS_NOW: { "methods": [ "POST", "GET" ], "path": "/apis/now", "params": [], "query": [] }
}
You can make API url by apifor
helpers by API ID
.
const endpoint = apifor ('apis.now')
// endpoint is resolved into '/apis/now'
Client Side Page Access Control
We provide user and grp base page access control.
<script>
export default {
setup (props, context) {
...
},
beforeRouteEnter (to, from, next) {
permission_required (['staff'], {name: 'signin'}, next)
}
}
</script>
admin
and staff
are pre-defined reserved grp name.
Vuex.state contains $uid
and $grp
state. So permission_required
check with
this state and decide to allow access.
And you should build sign in component signin.vue
.
Create backend/static/apps/main/signin.vue
.
<template>
<div>
<h1>Sign In</h1>
<input type="text" v-model='uid'>
<input type="password" v-model='password'>
<button @click='signin ()'>Sign In</button>
</div>
</template>
<script>
import { ref } from '/vue/composition-api.js'
import { signin_with_id_and_password, restore_route } from '/vue/helpers.js'
export default {
setup (props, context) {
const store = useStore ()
const uid = ref ('')
const password = ref ('')
const signin = async () => {
const msg = await signin_with_id_and_password (
'APIS_AUTH_SIGNIN_WITH_ID_AND_PASSWORD',
{uid: uid.value, password: password.value}
)
if (!!msg) {
return alert (`Sign in failed because ${ msg }`)
}
alert ('Sign in success!')
restore_route ()
}
return { uid, password, signin }
}
}
</script>
And one more, update /backend/static/apps/main/layout.vue
<script>
import { refresh_access_token } from '/vue/helpers.js'
import { onBeforeMount } from '/vue/composition-api.js'
export default {
setup () {
onBeforeMount ( () => {
refresh_access_token ('APIS_ACCESS_TOKEN')
})
}
}
</script>
This will check saved tokens at app initializing and do these things:
- update
Vuex.state.$uid
andVuex.state.$grp
if access token is valid - if access token is expired, try refresh using refresh token and save credential
- if refresh token close to expiration, refresh 'refresh token' itself
- if refresh token is expired, clear all credential
From this moment, axios
monitor access token
whenever you call APIs and automatically managing tokens.
Then we must create 2 APIs - API ID APIS_SIGNIN_WITH_ID_AND_PASSWORD
and
APIS_AUTH_ACCESS_TOKEN
.
Server Side Token Providing API
Update backend/services/apis.py
.
import time
USERS = {
'hansroh': ('1111', ['staff', 'user'])
}
def create_token (uid, grp = None):
due = (3600 * 6) if grp else (14400 * 21)
tk = dict (uid = uid, exp = int (time.time () + due))
if grp:
tk ['grp'] = grp
return tk
def __mount__ (app, mntopt):
@app.route ('/signin_with_id_and_password', methods = ['POST', 'OPTIONS'])
def signin_with_uid_and_password (was, uid, password):
passwd, grp = USERS.get (uid, (None, []))
if passwd != password:
raise was.Error ("401 Unauthorized", "invalid account")
return was.API (
refresh_token = was.mkjwt (create_token (uid)),
access_token = was.mkjwt (create_token (uid, grp))
)
@app.route ('/access_token', methods = ['POST', 'OPTIONS'])
def access_token (was, refresh_token):
claim = was.dejwt ()
atk = None
if 'err' not in claim:
atk = claim # valid token
elif claim ['ecd'] != 0: # corrupted token
raise was.Error ("401 Unauthorized", claim ['err'])
claim = was.dejwt (refresh_token)
if 'err' in claim:
raise was.Error ("401 Unauthorized", claim ['err'])
uid = claim ['uid']
_, grp = USERS.get (uid, (None, []))
rtk = was.mkjwt (create_token (uid)) if claim ['exp'] + 7 > time.time () else None
if not atk:
atk = create_token (uid, grp)
return was.API (
refresh_token = rtk,
access_token = was.mkjwt (atk)
)
You have responsabliity for these things.
- provide
access token
andrefresh token
access token
must containstr uid
,list grp
andint exp
refresh token
must containstr uid
andint exp
Now reload page, you can see Vuex.state.$apispecs
like this.
{
APIS_NOW: { "methods": [ "POST", "GET" ], "path": "/apis/now", "params": [], "query": [] },
APIS_ACCESS_TOKEN: { "methods": [ "POST", "OPTIONS" ], "path": "/apis/access_token", "params": [], "query": [ "refresh_token" ] },
APIS_SIGNIN_WITH_ID_AND_PASSWORD: { "methods": [ "POST", "OPTIONS" ], "path": "/apis/signin_with_id_and_password", "params": [], "query": [ "uid", "password" ] }
}
That's it.
Server Side Access Control
def __mount__ (app, mntopt):
@app.route ('/profiles/<uid>')
@app.permission_required (['user'])
def get_profile (was):
icanaccess = was.request.user.uid
return was.API (profile = data)
If request user is one of user
, staff
and admin
grp, access will be granted.
And all claims of access token can be access via was.request.user
dictionary.
@app.permission_required
can groups
and owner
based control.
Also @app.login_required
which is shortcut for @app.permission_required ([])
- any groups will be granted.
@app.identification_required
is just create was.request.user
object using access token only if token is valid.
For more detail access control. see Atila.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Hashes for atila_vue-0.3.0-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 459d75e23dcdede17b3ce7d23f60b3d3657b7597ec6c06a44a0efdce49845452 |
|
MD5 | d1aebe3427a49453476061fd49772784 |
|
BLAKE2b-256 | 6fc2607f79d82d9caf804fd3d46bb1d7b70ac904f76098ad573cb1837052cf4e |