atila-vue


Nameatila-vue JSON
Version 0.4.0 PyPI version JSON
download
home_pagehttps://gitlab.com/atila-ext/atila-vue
SummaryAtila Extension For VueJS 2 SFC and SSR
upload_time2023-07-08 10:40:56
maintainer
docs_urlNone
authorHans Roh
requires_python
licenseMIT
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            
## 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"
}
        
Elapsed time: 0.09033s