Skip to main content

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

Then create skitaid.py script for running server.

#! /usr/bin/env python3
import skitai
import atila_vue
import os
import app

os.environ ['SECRET_KEY'] = 'SECRET_KEY'

if __name__ == '__main__':
    with skitai.preference () as pref:
        pref.extends (atila_vue)
        skitai.mount ('/', app, pref)

    skitai.run (ip = '0.0.0.0', port = 5000, name = 'myservice')

But it doesn't work yet.

mkdir -p app

Then create app/__init__.py.

import atila
import os
import sys
import skitai

def __config__ (pref):
    pref.set_static ('/', 'app/static')
    pref.config.FRONTEND = {
        "googleAnalytics": {"id": "UA-158163406-1"}
    }

def __app__ ():
    return atila.Atila (__name__)

Now you can startup service.

./serve/py --devel

Then your browser address bar, enter http://localhost:5000/. If 404 Not Found on your browser screen, it is very OK.

Add Your First Page

Append these code into app/__init__.py.

def __mount__ (app, mntopt):
    @app.route ('/')
    def index (was):
        return '<h1>Hello, World</h1>'

Reload your browser then you can see Hello, World.

Improving Page With VueJS

mkdir app/templates

Create app/templates/main.j2.

{% extends '__framework/vue.j2' %}

Optionally, if you want to use Bootstrap 5 also.

{% extends '__framework/bs5.j2' %}

That's it. Just single line template.

Then update app/__init__.py for using this template.

def __mount__ (app, mntopt):
    @app.route ('/<path:path>')
    def index (was, path):
        return was.render ('main.j2')

Reload page but you will meet error: No page components for VueRouter.

Creating Vue Component

mkdir app/static/apps

This is routing base directory.

Your template file name is main.j2, so make directory app/static/apps/main as same name.

mkdir app/static/apps/main

Create file app/static/apps/main/layout.vue for app layout.

<template>
  <nav class='navbar navbar-expand-lg bg-dark navbar-dark'>
    <div class="container">
      <a href="#" class="navbar-brand">Atila Vue</a>
    </div>
  </nav>

  <router-view v-slot="{ Component }">
    <transition>
      <keep-alive>
        <component :is="Component" />
      </keep-alive>
    </transition>
  </router-view>
</template>

<script>
    export default {}
</script>

Create file app/static/apps/main/index.vue.

<template>
  <div class="container">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script>
  import {ref} from '/vue/composition-api.js'

  export default {
    setup () {
      const msg = ref ('Hello World')
      return { msg }
    }
  }
</script>

And Reload.

Note that you can see long list like Python Context and Vuex State. This will be shown if you give '--devel' option on starting server.

Add Routable Sub Pages

Create vue files as you want.

  • app/static/apps/main/about.vue
  • app/static/apps/main/products/index.vue
  • app/static/apps/main/products/_id.vue

These will be automatically generated as routes option like this:

[
    {name: "index", path: "/" },
    {name: "about", path: "/about" },
    {name: "products", path: "/products"},
    {name: "products/:id", path: "/products/:id")}
]

And you can see it via HTML source view in browser.

Then you can use <router-link> at your page.

<template>
  <div class="container">
    <h1>{{ msg }}</h1>
    <router-link :to="{ name: 'products/:id', params: {id: 100}}">Product #100</router-link>
  </div>
</template>

Using Vuex

You can define Vuex state.

Update app/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.

Creating Sub Apps

Add routes to app/__init__.py for createing My Page sub app.

def __mount__ (app, mntopt):
    @app.route ('/<path:path>')
    def index (was, path):
        return was.render ('main.j2')

    @app.route ('/mypage/<path:path>')
    def mypage (was, path):
        return was.render ('mypage.j2')

Then next steps are the same.

  • create app/templates/mypage.j2
  • create app/static/apps/mypage/index.vue and sub pages

Adding APIs

mkdir app/services

Create app/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 app/services/__init__.py

def __setup__ (app. mntopt):
  from . import apis
  app.mount ('/apis', apis)

Then update app/__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.

import { apifor } from '/vue/helpers.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.

<script>
  import { permission_required } from '/vue/helpers.js'

  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 app/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 /app/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 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 app/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 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.

{
  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.

Using Django ORM and Admin Site

Atila-Vue contains basic templates for using Django ORM management and Django admin site for laziness.

add one of these lines to skitaid.py as you prefer.

os.environ ['DBENGINE'] = 'sqlite3:///var/mydb.db3'
os.environ ['DBENGINE'] = 'postgresql://user:password@localhost:5432/mydb'
os.environ ['DBENGINE'] = 'oracle://user:password@localhost:1521/mydb'

Now, at shell,

./manage.py migrate

Some tables will be created on your database.

You can review these files:

  • app/models/config/settings.py

For adding models:

./manage.py startapp myapp

And add models and make admin customizations:

  • app/models/dap/myapp/models.py
  • app/models/dap/myapp/services.py
  • app/models/dap/myapp/admin.py

For applying:

./manage.py makemigrations
./manage.py migrate

Please note that Atila just use Django ORM management and admin site only. Using models is entirely different from conventional Django style. For usage examples using firebase_vue app:

  • app/services/apis/auth

And Django tutorials for managing ORM.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

atila_vue-0.2.1-py3-none-any.whl (194.7 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page