## Introduction
Atla-Vue is [Atila](https://pypi.org/project/atila/) extension package for
using [vue3-sfc-loader](https://github.com/FranckFreiburger/vue3-sfc-loader)
and [Bootstrap 5](https://getbootstrap.com/).
It will be useful for building simple web service at situation frontend developer
dose not exists.
Due to the [vue3-sfc-loader](https://github.com/FranckFreiburger/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
- Vuex
- Optional Bootstrap 5 for UI/UX
For injecting objects to Vuex, it uses [Jinja2](https://jinja.palletsprojects.com) template engine.
### Why Do I Need This?
- Single stack frontend developement, No 500M `node_modules`
- Optimized multiple small SPA/MPAs in single web service
- SSR and SEO advantage
### Full Example
See [atila-vue](https://gitlab.com/atila-ext/atila-vue) repository and [atila-vue examplet](https://gitlab.com/atila-ext/atila-vue/-/tree/master/example).
## Launching Server
```shell
mkdir myservice
cd myservice
```
`skitaid.py`
```python
#! /usr/bin/env python3
import skitai
import atila_vue
from atila import Allied
import backend
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`
```python
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-XXX-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.
```shell
./serve/py --devel
```
Then your browser address bar, enter `http://localhost:5000/ping`.
## Site Template
`backend/templates/site.j2`
```jinja
{% extends 'atila-vue/bs5.j2' %}
{% block lang %}en{% endblock %}
{% block state_map %}
{{ set_cloak (False) }}
{% endblock %}
```
## Multi Page App
`backend/__init__.py`
```python
def __mount__ (context, app):
import atila_vue
@app.route ('/')
@app.route ('/mpa')
def mpa (context):
return context.render (
'mpa.j2',
version = atila_vue.__version__
)
```
`backend/templates/mpa.j2`
```jinja
{% extends 'site.j2' %}
{% block content %}
{% include 'includes/header.j2' %}
<div class="container">
<h1>Multi Page App</h1>
</div>
{% endblock content %}
```
## Single Page App
`backend/__init__.py`
```python
def __mount__ (context, app):
import atila_vue
@app.route ('/spa/<path:path>')
def spa (context, path = None):
return context.render (
'spa.j2',
vue_config = dict (
use_router = context.baseurl (spa),
use_loader = True
),
version = atila_vue.__version__
)
```
`backend/templates/spa.j2`
```jinja
{% extends 'site.j2' %}
{% block content %}
{% include 'includes/header.j2' %}
{{ super () }}
{% endblock %}
```
As creating vue files, vue-router will be automatically configured.
- `backend/static/routes/spa/index.vue`: /spa
- `backend/static/routes/spa/sub.vue`: /spa/sub
- `backend/static/routes/spa/items/index.vue`: /spa/items
- `backend/static/routes/spa/items/_id.vue`: /spa/items/:id
### App Layout
`backend/static/routes/spa/__layout.vue`
```html
<template>
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in" appear>
<div :key="route.name">
<keep-alive>
<component :is="Component" />
</keep-alive>
</div>
</transition>
</router-view>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
<script setup>
const route = useRoute ()
</script>
```
### Optional Component To Use In Pages
`backend/static/routes/spa/components/myComponent.vue`
```html
<template>
My Component
</template>
<script setup>
</script>
```
### Route Pages
`backend/static/routes/spa/index.vue`
```html
<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 setup>
import myComponent from '/routes/spa/components/myComponent.vue'
const msg = ref ('hello world!')
</script>
```
`backend/static/routes/spa/sub.vue`
```html
<template>
<div class="container">
<h1>Sub Page</h1>
<div><router-link :to="{ name: 'index'}">Main Page</router-link></div>
</div>
</template>
```
`backend/static/routes/spa/items/index.vue`
```html
<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>
```
`backend/static/routes/spa/items/_id.vue`
```html
<template>
<div class="container">
<h1 class='ko-b'>Item {{ item_id }}</h1>
</div>
</template>
<style scoped>
.example {
color: v-bind('color');
}
</style>
<script setup>
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}`)
}
)
</script>
<script>
export default {
beforeRouteEnter (to, from, next) {
next ()
}
}
</script>
```
## Using Vuex: Injection
### Adding States
`backend/templates/mpa.j2`.
```jinja
{% extends '__framework/bs5.j2' %}
{% block state_map %}
{{ map_state ('page_id', 0) }}
{{ map_state ('types', ["todo", "canceled", "done"]) }}
{% endblock %}
```
These will be injected to `Vuex` through JSON.
Now tou can use these state on your vue file with `useStore`.
```html
<script setup>
const { state } = useStore ()
const page_id = computed ( () => state.page_id )
const msg = ref ('Hello World')
</script>
```
### Cloaking Control
`backend/templates/mpa.j2`.
```jinja
{% extends '__framework/bs5.j2' %}
{% block state_map %}
{{ set_cloak (True) }}
{% endblock %}
```
`index.vue` or nay vue
```html
<script setup>
onMounted (async () => {
await sleep (10000) // 10 sec
set_cloak (false)
})
</script>
```
### State Injection Macros
- `map_state (name, value, container = '', list_size = -1)`
- `map_dict (name, **kargs)`
- `map_text (name, container)`
- `map_html (name, container)`
- `set_cloak (flag = True)`
- `map_route (**kargs)`
## JWT Authorization And Access Control
### Basic About API
#### Adding API
`backend/services/apis.py`
```python
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`
```python
def __setup__ (app. mntopt):
from . import apis
app.mount ('/apis', apis)
```
Then update `backend/__init__.py` for mount `services`.
```python
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.
#### Accessing API
```html
<script 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
})
</script>
```
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`.
```js
{
APIS_NOW: { "methods": [ "POST", "GET" ], "path": "/apis/now", "params": [], "query": [] }
}
```
You can make API url by `backend.endpoint` helpers by `API ID`.
```js
const endpoint = backend.endpoint ('APIS_NOW')
// endpoint is resolved into '/apis/now'
```
### Access Control
#### Creating Server Side Token Providing API
Update `backend/services/apis.py`.
```python
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` and `refresh token`
- `access token` must contain `str uid`, `list grp` and `int exp`
- `refresh token` must contain `str uid` and `int exp`
Now reload page, you can see `Vuex.state.$apispecs` like this.
```js
{
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" ] }
}
```
#### Client Side Page Access Control
We provide user and grp base page access control.
```html
<script>
export default {
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/routes/main/signin.vue`.
```js
<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 setup>
const { state } = useStore ()
const uid = ref ('')
const password = ref ('')
async function signin () {
const msg = await backend.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!')
permission_granted () // go to origin route
}
</script>
```
And one more, update `/backend/static/routes/main/__layout.vue`
```js
<script setup>
onBeforeMount ( () => {
backend.refresh_access_token ('APIS_ACCESS_TOKEN')
})
</script>
```
This will check saved tokens at app initializing and do these things:
- update `Vuex.state.$uid` and `Vuex.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 Access Control
```python
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](https://pypi.org/project/atila/).
## Appendix
### Jinja Template Helpers
#### Globals
- `raise`
- `http_error (status, *args)`: raise context.HttpError
#### Filters
- `vue (val)`
- `summarize (val, chars = 60)`
- `attr (val)`
- `upselect (val, *names, **upserts)`
- `tojson_with_datetime (data)`
#### Macros
- `component (path, alias = none, _async = True)`
- `global_component (path, alias = none, _async = True)`
#### State Injection Macros
- `map_state (name, value, container = '', list_size = -1)`
- `map_dict (name, **kargs)`
- `map_text (name, container)`
- `map_html (name, container)`
- `set_cloak (flag = True)`
- `map_route (**kargs)`
### Javascript Helpers
#### Aliases
- `$http`: axios
#### Prototype Methods
- `Number.prototype.format`
- `String.prototype.format`
- `String.prototype.titleCase`
- `Date.prototype.unixepoch`
- `Date.prototype.format`
- `String.prototype.repeat`
- `String.prototype.zfill`
- `Number.prototype.zfill`
#### Device Detecting
- `device`
- `android`
- `ios`
- `mobile`
- `touchable`
- `rotatable`
- `width`
- `height`
#### Service Worker Sync
- `swsync`
- `async add_tag (tag, min_interval_sec = 0)`
- `async unregister_tag (tag)`: periodic only
- `async periodic_sync_enabled ()`
- `async is_tag_registered (tag)`
#### Backend URL Building and Authorization
- `backend`
- `endpoint (name, args = [], _kargs = {})`
- `static (relurl)`
- `media (relurl)`
- `async signin_with_id_and_password (endpoint, payload)`
- `async refresh_access_token (endpoint)`: In `onBeforeMount` at `__layout.vue`
- `create_websocket (API_ID, read_handler = (evt) => log (evt.data))`
- `push(msg)`
#### Logging
- `log (msg, type = 'info')`
- `traceback (e)`
#### Utilities
- `permission_required (permission, redirect, next)`: In `beforeRouteEnter`
- `permission_granted ()`: go to original requested route after signing in
- `build_url (baseurl, params = {})`
- `push_alarm (title, message, icon, timeout = 5000)`
- `load_script (src, callback = () => {})`: load CDN js
- `set_cloak (flag)`
- `async sleep (ms)`
- `async keep_offset_bottom (css, margin = 0, initial_delay = 0)`
- `async keep_offset_right (css, margin = 0, initial_delay = 0)`
Raw data
{
"_id": null,
"home_page": "https://gitlab.com/atila-ext/atila-vue",
"name": "atila-vue",
"maintainer": "",
"docs_url": null,
"requires_python": "",
"maintainer_email": "",
"keywords": "",
"author": "Hans Roh",
"author_email": "hansroh@gmail.com",
"download_url": "https://pypi.python.org/pypi/atila-vue",
"platform": "posix",
"description": "\n## Introduction\n\nAtla-Vue is [Atila](https://pypi.org/project/atila/) extension package for\nusing [vue3-sfc-loader](https://github.com/FranckFreiburger/vue3-sfc-loader)\nand [Bootstrap 5](https://getbootstrap.com/).\n\nIt will be useful for building simple web service at situation frontend developer\ndose not exists.\n\nDue to the [vue3-sfc-loader](https://github.com/FranckFreiburger/vue3-sfc-loader),\nWe can use **vue single file component** on the fly without any compiling or\nbuilding process.\n\nAtila-Vue composes these things:\n\n- VueJS 3\n- VueRouter\n- Vuex\n- Optional Bootstrap 5 for UI/UX\n\nFor injecting objects to Vuex, it uses [Jinja2](https://jinja.palletsprojects.com) template engine.\n\n### Why Do I Need This?\n- Single stack frontend developement, No 500M `node_modules`\n- Optimized multiple small SPA/MPAs in single web service\n- SSR and SEO advantage\n\n### Full Example\nSee [atila-vue](https://gitlab.com/atila-ext/atila-vue) repository and [atila-vue examplet](https://gitlab.com/atila-ext/atila-vue/-/tree/master/example).\n\n\n\n\n\n## Launching Server\n```shell\nmkdir myservice\ncd myservice\n```\n\n`skitaid.py`\n```python\n#! /usr/bin/env python3\nimport skitai\nimport atila_vue\nfrom atila import Allied\nimport backend\n\nif __name__ == '__main__':\n with skitai.preference () as pref:\n skitai.mount ('/', Allied (atila_vue, backend), pref)\n skitai.run (ip = '0.0.0.0', name = 'myservice')\n```\n\n`backend/__init__.py`\n```python\nimport skitai\n\ndef __config__ (pref):\n pref.set_static ('/', skitai.joinpath ('backend/static'))\n pref.config.MAX_UPLOAD_SIZE = 1 * 1024 * 1024 * 1024\n pref.config.FRONTEND = {\n \"googleAnalytics\": {\"id\": \"UA-XXX-1\"}\n }\n\ndef __app__ ():\n import atila\n return atila.Atila (__name__)\n\ndef __mount__ (context, app):\n import atila_vue\n\n @app.route (\"/api\")\n def api (context):\n return {'version': atila_vue.__version__}\n\n @app.route (\"/ping\")\n def ping (context):\n return 'pong'\n```\n\nNow you can startup service.\n```shell\n./serve/py --devel\n```\nThen your browser address bar, enter `http://localhost:5000/ping`.\n\n\n\n\n\n\n\n## Site Template\n`backend/templates/site.j2`\n```jinja\n{% extends 'atila-vue/bs5.j2' %}\n{% block lang %}en{% endblock %}\n{% block state_map %}\n {{ set_cloak (False) }}\n{% endblock %}\n```\n\n\n\n\n\n\n## Multi Page App\n`backend/__init__.py`\n```python\ndef __mount__ (context, app):\n import atila_vue\n @app.route ('/')\n @app.route ('/mpa')\n def mpa (context):\n return context.render (\n 'mpa.j2',\n version = atila_vue.__version__\n )\n```\n\n`backend/templates/mpa.j2`\n```jinja\n{% extends 'site.j2' %}\n{% block content %}\n {% include 'includes/header.j2' %}\n <div class=\"container\">\n <h1>Multi Page App</h1>\n </div>\n{% endblock content %}\n```\n\n\n\n\n\n\n\n## Single Page App\n`backend/__init__.py`\n```python\ndef __mount__ (context, app):\n import atila_vue\n @app.route ('/spa/<path:path>')\n def spa (context, path = None):\n return context.render (\n 'spa.j2',\n vue_config = dict (\n use_router = context.baseurl (spa),\n use_loader = True\n ),\n version = atila_vue.__version__\n )\n```\n\n`backend/templates/spa.j2`\n```jinja\n{% extends 'site.j2' %}\n{% block content %}\n {% include 'includes/header.j2' %}\n {{ super () }}\n{% endblock %}\n```\n\nAs creating vue files, vue-router will be automatically configured.\n- `backend/static/routes/spa/index.vue`: /spa\n- `backend/static/routes/spa/sub.vue`: /spa/sub\n- `backend/static/routes/spa/items/index.vue`: /spa/items\n- `backend/static/routes/spa/items/_id.vue`: /spa/items/:id\n\n\n### App Layout\n`backend/static/routes/spa/__layout.vue`\n```html\n<template>\n <router-view v-slot=\"{ Component }\">\n <transition name=\"fade\" mode=\"out-in\" appear>\n <div :key=\"route.name\">\n <keep-alive>\n <component :is=\"Component\" />\n </keep-alive>\n </div>\n </transition>\n </router-view>\n</template>\n\n<style>\n.fade-enter-active,\n.fade-leave-active {\n transition: opacity 0.3s ease;\n}\n.fade-enter-from,\n.fade-leave-to {\n opacity: 0;\n}\n</style>\n\n<script setup>\n const route = useRoute ()\n</script>\n```\n\n\n### Optional Component To Use In Pages\n`backend/static/routes/spa/components/myComponent.vue`\n```html\n<template>\n My Component\n</template>\n\n<script setup>\n</script>\n```\n\n\n### Route Pages\n`backend/static/routes/spa/index.vue`\n```html\n<template>\n <div class=\"container\">\n <h1>Main Page</h1>\n <span class=\"example\">\n <i class=\"bi-alarm\"></i>{{ msg }}</span>\n <div><router-link :to=\"{ name: 'sub'}\">Sub Page</router-link></div>\n <div><router-link :to=\"{ name: 'items'}\">Items</router-link></div>\n <div><my-component></my-component></div>\n </div>\n</template>\n\n<script setup>\n import myComponent from '/routes/spa/components/myComponent.vue'\n const msg = ref ('hello world!')\n</script>\n```\n\n\n`backend/static/routes/spa/sub.vue`\n```html\n<template>\n <div class=\"container\">\n <h1>Sub Page</h1>\n <div><router-link :to=\"{ name: 'index'}\">Main Page</router-link></div>\n </div>\n</template>\n```\n\n\n`backend/static/routes/spa/items/index.vue`\n```html\n<template>\n <div class=\"container\">\n <h1>Items</h1>\n <ul>\n <li v-for=\"index in 100\" :key=\"index\">\n <router-link :to=\"{ name: 'items/:id', params: {id: index}}\">Item {{ index }}</router-link>\n </li>\n </ul>\n </div>\n</template>\n```\n\n\n`backend/static/routes/spa/items/_id.vue`\n```html\n<template>\n <div class=\"container\">\n <h1 class='ko-b'>Item {{ item_id }}</h1>\n </div>\n</template>\n\n<style scoped>\n .example {\n color: v-bind('color');\n }\n</style>\n\n<script setup>\n const route = useRoute ()\n const item_id = ref (route.params.id)\n onActivated (() => {\n item_id.value = route.params.id\n })\n watch (() => item_id.value,\n (to, from) => {\n log (`item changed: ${from} => ${to}`)\n }\n )\n</script>\n\n<script>\n export default {\n beforeRouteEnter (to, from, next) {\n next ()\n }\n }\n</script>\n```\n\n\n\n\n\n\n\n\n\n\n## Using Vuex: Injection\n\n### Adding States\n`backend/templates/mpa.j2`.\n```jinja\n{% extends '__framework/bs5.j2' %}\n\n{% block state_map %}\n {{ map_state ('page_id', 0) }}\n {{ map_state ('types', [\"todo\", \"canceled\", \"done\"]) }}\n{% endblock %}\n```\n\nThese will be injected to `Vuex` through JSON.\n\nNow tou can use these state on your vue file with `useStore`.\n```html\n<script setup>\n const { state } = useStore ()\n const page_id = computed ( () => state.page_id )\n const msg = ref ('Hello World')\n</script>\n```\n\n### Cloaking Control\n`backend/templates/mpa.j2`.\n```jinja\n{% extends '__framework/bs5.j2' %}\n\n{% block state_map %}\n {{ set_cloak (True) }}\n{% endblock %}\n```\n\n`index.vue` or nay vue\n```html\n<script setup>\nonMounted (async () => {\n await sleep (10000) // 10 sec\n set_cloak (false)\n})\n</script>\n```\n\n\n\n### State Injection Macros\n- `map_state (name, value, container = '', list_size = -1)`\n- `map_dict (name, **kargs)`\n- `map_text (name, container)`\n- `map_html (name, container)`\n- `set_cloak (flag = True)`\n- `map_route (**kargs)`\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n## JWT Authorization And Access Control\n\n### Basic About API\n\n#### Adding API\n`backend/services/apis.py`\n```python\ndef __mount__ (app. mntopt):\n @app.route (\"\")\n def index (was):\n return \"API Index\"\n\n @app.route (\"/now\")\n def now (was):\n return was.API (result = time.time ())\n```\n\nCreate `backend/services/__init__.py`\n```python\ndef __setup__ (app. mntopt):\n from . import apis\n app.mount ('/apis', apis)\n```\n\nThen update `backend/__init__.py` for mount `services`.\n```python\ndef __app__ ():\n return atila.Atila (__name__)\n\ndef __setup__ (app, mntopt):\n from . import services\n app.mount ('/', services)\n\ndef __mount__ (app, mntopt):\n @app.route ('/')\n def index (was):\n return was.render ('main.j2')\n```\n\nNow you can use API: http://localhost:5000/apis/now.\n\n\n#### Accessing API\n```html\n<script setup>\n const msg = ref ('Hello World')\n const server_time = ref (null)\n onBeforeMount ( () => {\n const r = await $http.get ('/apis/now')\n server_time.value = r.data.result\n })\n</script>\n```\n\n\nVuex.state has `$apispecs` state and it contains all API specification of server side. We made only 1 APIs for now.\n\n**Note** that your exposed APIs endpoint should be `/api`.\n```js\n{\n APIS_NOW: { \"methods\": [ \"POST\", \"GET\" ], \"path\": \"/apis/now\", \"params\": [], \"query\": [] }\n}\n```\nYou can make API url by `backend.endpoint` helpers by `API ID`.\n```js\nconst endpoint = backend.endpoint ('APIS_NOW')\n// endpoint is resolved into '/apis/now'\n```\n\n\n### Access Control\n\n#### Creating Server Side Token Providing API\n\nUpdate `backend/services/apis.py`.\n```python\nimport time\n\nUSERS = {\n 'hansroh': ('1111', ['staff', 'user'])\n}\n\ndef create_token (uid, grp = None):\n due = (3600 * 6) if grp else (14400 * 21)\n tk = dict (uid = uid, exp = int (time.time () + due))\n if grp:\n tk ['grp'] = grp\n return tk\n\ndef __mount__ (app, mntopt):\n @app.route ('/signin_with_id_and_password', methods = ['POST', 'OPTIONS'])\n def signin_with_uid_and_password (was, uid, password):\n passwd, grp = USERS.get (uid, (None, []))\n if passwd != password:\n raise was.Error (\"401 Unauthorized\", \"invalid account\")\n return was.API (\n refresh_token = was.mkjwt (create_token (uid)),\n access_token = was.mkjwt (create_token (uid, grp))\n )\n\n @app.route ('/access_token', methods = ['POST', 'OPTIONS'])\n def access_token (was, refresh_token):\n claim = was.dejwt ()\n atk = None\n if 'err' not in claim:\n atk = claim # valid token\n elif claim ['ecd'] != 0: # corrupted token\n raise was.Error (\"401 Unauthorized\", claim ['err'])\n\n claim = was.dejwt (refresh_token)\n if 'err' in claim:\n raise was.Error (\"401 Unauthorized\", claim ['err'])\n\n uid = claim ['uid']\n _, grp = USERS.get (uid, (None, []))\n rtk = was.mkjwt (create_token (uid)) if claim ['exp'] + 7 > time.time () else None\n\n if not atk:\n atk = create_token (uid, grp)\n\n return was.API (\n refresh_token = rtk,\n access_token = was.mkjwt (atk)\n )\n```\n\nYou have responsabliity for these things.\n- provide `access token` and `refresh token`\n- `access token` must contain `str uid`, `list grp` and `int exp`\n- `refresh token` must contain `str uid` and `int exp`\n\nNow reload page, you can see `Vuex.state.$apispecs` like this.\n```js\n{\n APIS_NOW: { \"methods\": [ \"POST\", \"GET\" ], \"path\": \"/apis/now\", \"params\": [], \"query\": [] },\n\n APIS_ACCESS_TOKEN: { \"methods\": [ \"POST\", \"OPTIONS\" ], \"path\": \"/apis/access_token\", \"params\": [], \"query\": [ \"refresh_token\" ] },\n\n APIS_SIGNIN_WITH_ID_AND_PASSWORD: { \"methods\": [ \"POST\", \"OPTIONS\" ], \"path\": \"/apis/signin_with_id_and_password\", \"params\": [], \"query\": [ \"uid\", \"password\" ] }\n}\n```\n\n\n\n\n\n#### Client Side Page Access Control\n\nWe provide user and grp base page access control.\n```html\n<script>\n export default {\n beforeRouteEnter (to, from, next) {\n permission_required (['staff'], {name: 'signin'}, next)\n }\n }\n</script>\n```\n`admin` and `staff` are pre-defined reserved grp name.\n\nVuex.state contains `$uid` and `$grp` state. So `permission_required` check with\nthis state and decide to allow access.\n\nAnd you should build sign in component `signin.vue`.\n\nCreate `backend/static/routes/main/signin.vue`.\n```js\n<template>\n <div>\n <h1>Sign In</h1>\n <input type=\"text\" v-model='uid'>\n <input type=\"password\" v-model='password'>\n <button @click='signin ()'>Sign In</button>\n </div>\n</template>\n\n<script setup>\n const { state } = useStore ()\n const uid = ref ('')\n const password = ref ('')\n async function signin () {\n const msg = await backend.signin_with_id_and_password (\n 'APIS_AUTH_SIGNIN_WITH_ID_AND_PASSWORD',\n {uid: uid.value, password: password.value}\n )\n if (!!msg) {\n return alert (`Sign in failed because ${ msg }`)\n }\n alert ('Sign in success!')\n permission_granted () // go to origin route\n }\n</script>\n```\n\n\n\n\nAnd one more, update `/backend/static/routes/main/__layout.vue`\n```js\n<script setup>\n onBeforeMount ( () => {\n backend.refresh_access_token ('APIS_ACCESS_TOKEN')\n })\n</script>\n```\nThis will check saved tokens at app initializing and do these things:\n- update `Vuex.state.$uid` and `Vuex.state.$grp` if access token is valid\n- if access token is expired, try refresh using refresh token and save credential\n- if refresh token close to expiration, refresh 'refresh token' itself\n- if refresh token is expired, clear all credential\n\nFrom this moment, `axios` monitor `access token` whenever you call APIs and automatically managing tokens.\n\nThen we must create 2 APIs - API ID `APIS_SIGNIN_WITH_ID_AND_PASSWORD` and\n`APIS_AUTH_ACCESS_TOKEN`.\n\n\n\n\n\n\n#### Server Side Access Control\n```python\ndef __mount__ (app, mntopt):\n @app.route ('/profiles/<uid>')\n @app.permission_required (['user'])\n def get_profile (was):\n icanaccess = was.request.user.uid\n return was.API (profile = data)\n```\nIf request user is one of `user`, `staff` and `admin` grp, access will be granted.\n\nAnd all claims of access token can be access via `was.request.user` dictionary.\n\n`@app.permission_required` can `groups` and `owner` based control.\n\nAlso `@app.login_required` which is shortcut for `@app.permission_required ([])` - any groups will be granted.\n\n`@app.identification_required` is just create `was.request.user` object using access token only if token is valid.\n\nFor more detail access control. see [Atila](https://pypi.org/project/atila/).\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n## Appendix\n\n### Jinja Template Helpers\n\n#### Globals\n- `raise`\n- `http_error (status, *args)`: raise context.HttpError\n\n#### Filters\n- `vue (val)`\n- `summarize (val, chars = 60)`\n- `attr (val)`\n- `upselect (val, *names, **upserts)`\n- `tojson_with_datetime (data)`\n\n#### Macros\n- `component (path, alias = none, _async = True)`\n- `global_component (path, alias = none, _async = True)`\n\n\n#### State Injection Macros\n- `map_state (name, value, container = '', list_size = -1)`\n- `map_dict (name, **kargs)`\n- `map_text (name, container)`\n- `map_html (name, container)`\n- `set_cloak (flag = True)`\n- `map_route (**kargs)`\n\n\n\n\n\n### Javascript Helpers\n#### Aliases\n- `$http`: axios\n\n#### Prototype Methods\n- `Number.prototype.format`\n- `String.prototype.format`\n- `String.prototype.titleCase`\n- `Date.prototype.unixepoch`\n- `Date.prototype.format`\n- `String.prototype.repeat`\n- `String.prototype.zfill`\n- `Number.prototype.zfill`\n\n#### Device Detecting\n- `device`\n - `android`\n - `ios`\n - `mobile`\n - `touchable`\n - `rotatable`\n - `width`\n - `height`\n\n#### Service Worker Sync\n- `swsync`\n - `async add_tag (tag, min_interval_sec = 0)`\n - `async unregister_tag (tag)`: periodic only\n - `async periodic_sync_enabled ()`\n - `async is_tag_registered (tag)`\n\n#### Backend URL Building and Authorization\n- `backend`\n - `endpoint (name, args = [], _kargs = {})`\n - `static (relurl)`\n - `media (relurl)`\n - `async signin_with_id_and_password (endpoint, payload)`\n - `async refresh_access_token (endpoint)`: In `onBeforeMount` at `__layout.vue`\n - `create_websocket (API_ID, read_handler = (evt) => log (evt.data))`\n - `push(msg)`\n\n\n#### Logging\n- `log (msg, type = 'info')`\n- `traceback (e)`\n\n#### Utilities\n- `permission_required (permission, redirect, next)`: In `beforeRouteEnter`\n- `permission_granted ()`: go to original requested route after signing in\n- `build_url (baseurl, params = {})`\n- `push_alarm (title, message, icon, timeout = 5000)`\n- `load_script (src, callback = () => {})`: load CDN js\n- `set_cloak (flag)`\n- `async sleep (ms)`\n- `async keep_offset_bottom (css, margin = 0, initial_delay = 0)`\n- `async keep_offset_right (css, margin = 0, initial_delay = 0)`\n\n\n\n\n\n\n\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Atila Extension For VueJS 2 SFC and SSR",
"version": "0.4.0",
"project_urls": {
"Download": "https://pypi.python.org/pypi/atila-vue",
"Homepage": "https://gitlab.com/atila-ext/atila-vue"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "9c492b3ec1bbfbc9970d262346e4f69b812fa220a7079735179f3da3ddf87a81",
"md5": "9683e7b2ed100ae597ed40aae5bb2cd7",
"sha256": "0b8d6028b029c6350cba69842073d3fdc0a14c606c68be377c78f2b102e2be3a"
},
"downloads": -1,
"filename": "atila_vue-0.4.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "9683e7b2ed100ae597ed40aae5bb2cd7",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 148095,
"upload_time": "2023-07-08T10:40:56",
"upload_time_iso_8601": "2023-07-08T10:40:56.173925Z",
"url": "https://files.pythonhosted.org/packages/9c/49/2b3ec1bbfbc9970d262346e4f69b812fa220a7079735179f3da3ddf87a81/atila_vue-0.4.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-07-08 10:40:56",
"github": false,
"gitlab": true,
"bitbucket": false,
"codeberg": false,
"gitlab_user": "atila-ext",
"gitlab_project": "atila-vue",
"lcname": "atila-vue"
}