Metadata-Version: 2.1
Name: atila-vue
Version: 0.3.0
Summary: Atila Extension For VueJS 2 SFC and SSR
Home-page: https://gitlab.com/atila-ext/atila-vue
Author: Hans Roh
Author-email: hansroh@gmail.com
License: MIT
Download-URL: https://pypi.python.org/pypi/atila-vue
Platform: posix
Classifier: License :: OSI Approved :: MIT License
Classifier: Development Status :: 1 - Planning
Classifier: Environment :: Web Environment
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: JavaScript
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: atila
Requires-Dist: jinja2


## 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 4
- Vuex 4
- Optional Bootstrap 5 for UI/UX

For injecting objects to Vuex, it uses [Jinja2](https://jinja.palletsprojects.com) template engine.

### 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

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`
```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-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.
```shell
./serve/py --devel
```
Then your browser address bar, enter `http://localhost:5000/ping`.


Again, `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__,
          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`
```jinja
{% extends 'atila-vue/bs5.j2' %}
{% block lang %}en{% endblock %}
{% block state_map %}
    {{ set_cloak (False) }}
{% endblock %}
```



### Multi Page App Template
`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 Template
`backend/templates/spa.j2`
```jinja
{% 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`: /spa
- `backend/static/apps/spa/sub.vue`: /spa/sub
- `backend/static/apps/spa/items/index.vue`: /spa/items
- `backend/static/apps/spa/items/_id.vue`: /spa/items/:id


### App Layout
`backend/static/apps/spa/layout.vue`
```html
<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`
```html
<template>
  My Component
</template>

<script>
  export default {}
</script>
```


### Route Pages
`backend/static/apps/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>
  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`
```html
<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`
```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>

<script>
  export default {}
</script>
```



`backend/static/apps/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>
  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`.
```jinja
{% 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`.
```html
<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`.
```html
<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](https://gitlab.com/atila-ext/atila-vue/-/blob/master/atila_vue/static/vue/helpers.js) contains some shortcuts for `Vue.`, `Vuex.` and `VueRouter`.







## Adding APIs
```shell
mkdir backend/services
```

Create `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.

```html
<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`.
```js
{
  APIS_NOW: { "methods": [ "POST", "GET" ], "path": "/apis/now", "params": [], "query": [] }
}
```
You can make API url by `apifor` helpers by `API ID`.
```js
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.
```html
<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`.
```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>
    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`
```js
<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` 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 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" ] }
}
```

That's it.






## 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/).


