Djangoで公開WebアプリをFirebase認証で作る

Djangoでの開発の勉強として、練習用に以下のようなサンプルアプリを作る。

  • 極力ややこしいことはせず、Google、Twitter、Facebook等でユーザ登録や認証(ログイン)ができる。
  • 「サークル」というユーザが集まる単位があり、ユーザは複数のサークルに所属する。
  • あらゆるユーザは新しいサークルを作成できる。
  • サークルへの参加は、そのサークルにすでに参加しているメンバーの招待により可能となる。
  • サークルごとには、簡単なメッセージボードがあってメッセージをやり取りできる。

技術的な制約としては以下とする。

  • フロントエンド開発はなし。ReactやAngularなんかのフレームワーク使わないし、Javascriptも最低限にする。画面遷移はDjangoの基本的な使い方で、TemplateのHTMLを使う。

以降の手順の説明において、解説はポイントだけとしてDjangoの学習レベルの説明はしない。

テスト用のアプリを用意して動かす

まずはDjangoをインストールしてStartAppまで行く。

# Homeにアプリ用のディレクトリを作成
user@sv:~$ mkdir simplecircle
user@sv:~$ cd ./simplecircle

user@sv:~/simplecircle$ python3 -V
Python 3.10.6

# VirtualEnv環境を作成
user@sv:~/simplecircle$ python3 -m venv scenv
user@sv:~/simplecircle$ . ./scenv/bin/activate

# pipの更新、Djangoとグローバルログインモジュールを導入
(scenv) user@sv:~/simplecircle$ pip install -U pip
(省略)
Successfully installed pip-22.2.2

(scenv) user@sv:~/simplecircle$ pip install django
(省略)
Successfully installed asgiref-3.5.2 django-4.1.2 sqlparse-0.4.3

(scenv) user@sv:~/simplecircle$ pip install django-glrm
(省略)
Successfully installed django-glrm-1.1.3

(scenv) user@sv:~/simplecircle$ django-admin startproject simplecircle
(scenv) user@sv:~/simplecircle$ cd simplecircle/
(scenv) user@sv:~/simplecircle/simplecircle$ python manage.py startapp app

さて、Settingsの方はここだけいじる。

[simplecircle/settings.py]

ALLOWED_HOSTS = ['localhost']

INSTALLED_APPS = [
    ...
    'django.contrib.staticfiles',
    'app'
]

MIDDLEWARE = [
    ...
    'global_login_required.GlobalLoginRequiredMiddleware', <- 追加
    'simplecircle.middleware.AppMiddleware', <- 追加、後で作る
]

### 書き足す ↓↓↓
LOGIN_REDIRECT_URL = '/app/circle_board'
LOGOUT_REDIRECT_URL = '/accounts/login'

PUBLIC_PATHS = [
	'/accounts/',
]
### 書き足す ↑↑↑

まずはModelを作ってしまう。
Circleというのがこのアプリの人が集まっているサークルのマスタになる。属性は名前だけ。JoiningというのがManyToMany的なCircleとユーザの紐付けテーブル。
最初は使わないけども、サークルへの招待をすることになるのでその招待チケットのテーブルも作り、あとはメッセージボード的なところに流れるメッセージとした。

[app/models.py]
from django.db import models
from django.contrib.auth.models import User

# Circle - サークル
class Circle(models.Model):
    def __str__(self):
        return self.name
    name = models.CharField(max_length=64)

# Joining - ユーザがサークルに入っている紐付け
class Joining(models.Model):
    def __str__(self):
        return self.circle.name + " hires " + str(self.user)
    circle = models.ForeignKey(Circle, on_delete=models.CASCADE)
    user = models.ForeignKey(User, on_delete=models.CASCADE)

# CircleInvitation - サークルへの招待
class CircleInvitation(models.Model):
    def __str__(self):
        return 'invitation of ' + str(self.circle)
    circle = models.ForeignKey(Circle, on_delete=models.CASCADE)
    key = models.TextField('Random Hash')
    expired_at = models.DateTimeField('Expired At')

# Message - サークルの伝言板
class Message(models.Model):
    def __str__(self):
        return 'message at' + str(self.posted_at)
    circle = models.ForeignKey(Circle, on_delete=models.CASCADE)
    user_posted = models.ForeignKey(User, on_delete=models.CASCADE)
    body = models.TextField('Message Body')
    posted_at = models.DateTimeField('Posted At')

Modelが満足行く出来になったらもちろんこれ。

$ python manage.py makemigrations
$ python manage.py migrate

次にurl関係を。
Djangoの付属の管理画面(admin)も一旦有効にする。このままだと公開はできないような気もするけど設定を間違えなければ問題ないような気もする。accountsに関してはログイン認証関係をやってくれるのでいったん入れるが、ログイン画面を用意するくらいにしか使わない。
アプリ側の画面は1画面だけ。サークルの伝言表示&投稿画面だけとする。ユーザが複数のサークルに参加している場合はサークルの切り替えが必要なので、そのためのViewを一つ用意している。

[simplecircle/urls.py]
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('django.contrib.auth.urls')),
    path('app/',include('app.urls')),
]

[app/urls.py]
from django.urls import path
from . import views

app_name='app'
urlpatterns = [
    path('circle_board', views.circle_board, name='circle_board'),
    path('circle_switch/<int:to_circle>', views.circle_switch, name='circle_switch'),
]

Admin画面からテストデータを登録してみたいので

[app/admin.py]
from django.contrib import admin
from .models import *

admin.site.register(Circle)
admin.site.register(Joining)
admin.site.register(CircleInvitation)
admin.site.register(Message)

この辺まで来たらスーパーユーザをひとり、Mikeくんというのを作ってから、 Django の仮サーバを上げて見る。

$ python manage.py createsuperuser --username mike
Email address: mike@greatmail.com
Password: 
Password (again): 
Superuser created successfully.
$ python manage.py runserver 0.0.0.0:8000

Admin画面(http://localhost:8000/admin/)に入ってみて、テストデータを登録してみる。まずはギター練習と筋トレと骨董カメラという3つのサークルを登録した。

次にMikeくん以外にもKevinとAnnaを登録。

3人と3つのサークルを適当に紐つけてあげる。(annaは全部、kevinはカメラとギター、mikeは筋トレとギター)

いよいよViewを。そこまで複雑なことはしていないが、ModelFormを使っているのとredirectなんかの意味合いがわからない場合はDjangoの公式チュートリアルなどで確認するとよい。
cirecle_boardはサークルの伝言板ページになる。ひとことのメッセージが投稿可能なのでフォームを出しており、POSTデータが投稿されたらMessageクラスを利用してSaveを行っている。
circle_switchはrequest.session[‘circle’]というところに入るcircleのidを変更するだけというシンプルな処理をして伝言板にリダイレクトしている。一応、参加していない人は弾くという分岐だけは実装している。

[app/views.py]
from django.shortcuts import render, get_object_or_404, redirect
from django.http import Http404
from .models import *
from .forms import MessageForm
from django.utils import timezone as tz

def circle_board(request):

    context={}

    context['circle'] = get_object_or_404(Circle, pk=request.circle.id)
    context['swcs'] = Joining.objects.filter(user=request.user)\
        .exclude(circle=request.circle.id)
    context['form'] = MessageForm()
    context['messages'] = Message.objects.filter(circle=request.circle)

    if(request.method == 'POST'):

        p = request.POST
        m = Message(
            circle=request.circle,
            user_posted=request.user,
            body=p['body'],
            posted_at=tz.now(),
        )
        m.save()

        return redirect('app:circle_board')

    return render(request, 'circle_board.html', context)

def circle_switch(request, to_circle):

    j = Joining.objects.filter(user=request.user,circle=to_circle).first()
    if(j is None):
        raise Http404('Not be joining to this circle.')

    request.session['circle'] = to_circle

    return redirect('app:circle_board')

上で、MessageFormと出てきたModelformをappフォルダの下にforms.pyとして作成。

[app/forms.py]
from django import forms
from .models import Message

class MessageForm(forms.ModelForm):

    class Meta:
        model = Message
        fields = ["body"]

Templateを作成。まずはapp/temlatesフォルダを切った上で、そこにcircle_board画面のhtmlを置く。
大きく3ブロックあることを確認してほしい。
1: サークルの名称を大きく表示、ログイン中のユーザとログアウト用のリンクを表示。
2: 他のサークルのボードへ切り替え(circle_switch)をする
3: 伝言メッセージの投稿フォームと、投稿済みのメッセージの表示

[app/templates/circle_board.html]
<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>Circle Board</title>
  </head>
  <body>
    <h1>Circle: {{circle.name}}</h1>
    <p>Hi {{request.user.username}}. <a href="{% url 'logout' %}">Logout</a></p>


    <h2>Change Circle</h2>
      {% for c in swcs %}
        <p><a href="{% url 'app:circle_switch' c.circle.id %}">
          {{c.circle.name}}
        </a></p>
      {% endfor %}



    <h2>Message Board</h2>

    <form method="POST"> {% csrf_token %}
        <fieldset>
          {% for field in form %}
            <label>{{ field.label_tag }}</label>
            <div>
                {{ field }}
                {{ field.errors }}
            </div>
          {% endfor %}
          <input name="submit" type="submit" value="Post" />
        </fieldset>
    </form>

    {% for m in messages %}
    <p>
      {{m.user_posted.username}} sayid in {{m.posted_at}} : {{m.body}}
    </p>
    {% endfor %}


  </body>
</html>

それと、ログイン画面用に一つだけtemplatesの下にregistrationというフォルダを掘ってファイルを置く。

[app/templates/login.html]
</<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>Circle Board</title>
  </head>
  <body>

    <h2>Login</h2>
    <form method="post">
      {% csrf_token %}
      {{ form.as_p }}
      <button type="submit">Login</button>
    </form>

  </body>
</html>

あとは、ユーザが今どこのサークルを見ているのかをSessionで管理したいのでミドルウェアを一つ作る。やっていることはシンプルにいうと3つくらいで
「アクセスを受けたときにユーザがログオンしていてまだrequest.session[‘circle’]がセットされていない場合は適当なものを一つ選ぶ」
「request.session[‘circle’]にIDの数値が保存されていたら、Circleオブジェクトをrequest.circleにセットする=Viewでrequest.circleがいきなり使える」
「ログオンしているユーザがどこのCircleにも所属していない場合はエラーになる(return “error” とかいま時点は適当)」

from app.models import Circle, Joining
from django.shortcuts import redirect,get_object_or_404

def AppMiddleware(get_response):

    def middleware(request):

        if request.user.is_authenticated:
            if('circle' not in request.session):
                j = Joining.objects.filter(user=request.user).first()
                if j is None:

                    # the user is orfan!!
                    return "error"

                else:
                    # get company and set session from top hire records.
                    request.session['circle'] = j.circle.id
                    circle = j.circle

            else:
                circle = get_object_or_404(Circle, pk=request.session['circle'])

        else:
            circle = None

        request.circle = circle

        response = get_response(request)
        return response

    return middleware

さて動かして見ると、こんなアプリがちゃんと動いた。Mikeでログインしていてギターのサークルにいる。Mikeが参加している他のサークルとしては筋トレがあるのでChange Circleにはそれが表示されている。
Message Boardには他のユーザが投稿したメッセージも含めて表示されている。

これで基本的なアプリは作り終えたとする。今の所Circleの新規追加画面やユーザを新規に登録する画面はAdmin画面にしかないが、これは認証をFirebaseを使ってGoogleなどのソーシャルサービスとの連携で作る練習用の前提なので、そこへ進むことにする。

Firebase認証を導入

この簡単なアプリにFirebase認証を組み込んでいく。40時間くらい調べまくってようやく大体動くようになった。

ひとまずFirebaseにのアカウントを登録して以下の画面で </> のWebを選択し、あれこれみつつなんとなくこの画面までたどり着く。

メール/パスワード認証と、GoogleとTwitterあたりをログオン方法には入れておきたいので、ここはWebで調べながら実施。Googleは簡単だった気がするけど、TwitterについてはElevated Accessの申請というのが必要で少し手間取った。手順のメモをここは残していない。

Firebase側が大体整ったならば、アプリ側を作っていく。
まず、全体的に以下の構成となっている。結構ややこしいけど最低限認証出やりたいことをやっていくとほぼこれくらいにはなる気がする。

Portal

DjangoとしてViewを一つ切る。やっていることはFirebase Auth UIをほぼマニュアル通りに表示するだけ。
もしすでにDjangoアプリに認証済みであればアプリのトップページ(circle_board)に飛ばすが、そうでなければportal.htmlをレンダリングするだけ。

[views.py]
def portal(request):

    if(request.user.is_authenticated):
        return redirect('app:circle_board')

    return render(request, 'portal.html')

templates/portal.htmlは、ほぼマニュアル通りにFirebase Auth UIを表示させる。
ただ、firebaseそのもののマニュアルとかだとfirebase-app.jsとかを使わせようとしくるが、”-compat”付きのファイルを選ぶように注意。そうでなければmoduleとして利用しなければならなくなり、Javascriptあまり詳しくないけど、<script type=”module”>ってのには気をつけないと恐ろしい沼にはまる。あとconfigに関しては環境ごとにことなるので調べて設定が必要。signInSuccessURLというのが、次のBridgeというViewに飛ばしている部分になる。

[teemplates/portal.html]
{% load static %}
<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <script src="https://www.gstatic.com/firebasejs/9.11.0/firebase-app-compat.js"></script>
    <script src="https://www.gstatic.com/firebasejs/9.11.0/firebase-auth-compat.js"></script>
    <script type="text/javascript">
      var config = {
        apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        authDomain: "xxxxxxxxxxxx.firebaseapp.com",
        projectId: "xxxxxxxxxxxxxxxx",
        storageBucket: "xxxxxxxxxxxx.firebaseapp.com",
        messagingSenderId: "333333333333333",
        appId: "9:9999999999999:web:01234567890123456",
      };
      firebase.initializeApp(config);
    </script>
    <script src="https://www.gstatic.com/firebasejs/ui/3.1.1/firebase-ui-auth__{{request.LANGUAGE_CODE}}.js"></script>
    <link type="text/css" rel="stylesheet" href="https://www.gstatic.com/firebasejs/ui/3.1.1/firebase-ui-auth.css" />
    <title>SimpleCircle Logon</title>
  </head>
  <body>

    <h1>Simple Circle Service</h1>

    <div id="firebaseui-auth-container"></div>
    <div id="loader">Loading...</div>

    <script type="text/javascript">

      var uiConfig = {
        callbacks: {
          signInSuccessWithAuthResult: function(authResult, redirectUrl) {
            return true;
          },
          uiShown: function() {
            document.getElementById('loader').style.display = 'none';
          }
        },
        signInFlow: 'redirect',
        signInSuccessUrl: 'http://localhost:8000/app/bridge',
        signInOptions: [
          firebase.auth.EmailAuthProvider.PROVIDER_ID,
          firebase.auth.GoogleAuthProvider.PROVIDER_ID,
          firebase.auth.TwitterAuthProvider.PROVIDER_ID,
        ],

        tosUrl: 'http://localhost:8000/app/term_of_service',
        // Privacy policy url/callback.
        privacyPolicyUrl: 'http://localhost:8000/app/privacy_policy',

      };

      var ui = new firebaseui.auth.AuthUI(firebase.auth());
      ui.start('#firebaseui-auth-container', uiConfig);
    </script>
  </body>
  </body>
</html>

これで以下が(たぶん)表示される。

Bridge

こっちもDjangoのViewは全く同じ。

def bridge(request):

    if(request.user.is_authenticated):
        return redirect('app:circle_board')

    return render(request, 'bridge.html')

問題はHTMLとJavascriptでそこそこのややこしい。このHTMLは白紙のページしか見えなくなっている。
FirebaseのonAuthStatusChangedっていうのが、Firebase Authのブラウザのログオン情報が変更されたときにトリガされるものっていう説明になっているのだけど、どうやらページがロードされたらすぐ確認に行くようなのでPortalから飛ばされてきたときに起動するよう。

その時点でFirebase的にログオンできてない場合、Portalに返す。
ログオンできている場合は後で解説する「trytoken」というDjango側で自作するAPIに、Firebaseからuser.getIdToken()で受け取ったトークンを送って、Django側、つまりサーバサイドでもFirebaseにトークンを送って真贋判定を行っている。JSON.stringifyだけだと、なぜかトークンが”xxxxxx”ってダブルクォーテーションで囲われたままになってえーって思ったけど、ネットで調べてもよくわからず、勘でevalしてやったらなんかうまく行った。
そしてJqueryの$.postでAPIを検証させている。
APIが返してきた結果により
 FirebaseのTokenが通らなかった → Portalへリダイレクト
 Tokenが通り、ユーザIDで照合したらDjangoアプリとしても登録済みのユーザだった → ログオン処理はAPI側で終わらせてあげているので、アプリのトップページにリダイレクトする。
 Tokenが通ったが、お初の認証の人の場合、GoogleやTwitter認証の場合は一気にアプリ側のユーザ登録もAPIで終わっているので、アプリのトップページに迎え入れる。
 Tokenが通ってお初の人、またはメール/パスワード認証でメールアドレスの検証が終わっていない人の場合にはリンク付きのメールを送ってそのリンクを分でもらって初めてお迎えすることにするので<div id=”email_verify”>の部分を表示させてメールボックスを見るように誘導している。

{% load static %}
<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <script src="https://www.gstatic.com/firebasejs/9.11.0/firebase-app-compat.js"></script>
    <script src="https://www.gstatic.com/firebasejs/9.11.0/firebase-auth-compat.js"></script>
    <script type="text/javascript">
      var config = {
        apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        authDomain: "xxxxxxxxxxxx.firebaseapp.com",
        projectId: "xxxxxxxxxxxxxxxx",
        storageBucket: "xxxxxxxxxxxx.firebaseapp.com",
        messagingSenderId: "333333333333333",
        appId: "9:9999999999999:web:01234567890123456",
      };
      firebase.initializeApp(config);
    </script>
    <script type="text/javascript" src="{% static 'js/jquery/jquery.min.js' %}"></script>
    <title>SimpleCircle Logon</title>
  </head>
  <body>

    <div id="email_verify" class="email_verify" style="display: none;">
      <p>Email Verification mail is sent to your address.</p>
      <p>Please confirm it and follow the link. you can close this window.</p>
    </div>
    <script type="text/javascript">

      firebase.auth().onAuthStateChanged(user => {
        console.log(user.displayName)
        if (user == null){
          window.location.href = "{% url 'app:portal' %}";
        } else {

            user.getIdToken(true).then((data) => {

              var token = eval(JSON.stringify(data));

              $.post({
                  url: "{% url 'app:trytoken' %}",
                  data: {
                    'token': token,
                    'csrfmiddlewaretoken' : '{{ csrf_token }}'
                  },
                  dataType: 'json',
                  success: function(rjson){
                    console.log(rjson.vertoken);
                    if (rjson.vertoken == false) {
                      window.location.href = "{% url 'app:portal' %}";
                    }else if(rjson.dloggedin == true){
                      console.log('logged on to django, redirect.');
                      window.location.href = "{% url 'app:circle_board' %}";
                    }else {
                      if(rjson.emailvalid == false){
                        console.log('email has not verified yet.');
                        document.getElementById('email_verify').style.display = 'block';
                        var uri = new URL(window.location.href);
                        var actionCodeSettings = {
                          url: uri.origin + "{% url 'app:bridge' %}",
                          handleCodeInApp: false
                        };

                        firebase.auth().currentUser.sendEmailVerification(actionCodeSettings);
                      }
                    }
                  },
                  error: function(){
                    alert('Server Error');
                  }

              });
          });
        }
      });
    </script>
  </body>
  </body>
</html>

trytoken API

これはAjaxでFirebaseのトークンを検証する、Bridgeから呼んでいたもの。HTMLのTemplateはいらなくてDjangoのapp/views.pyだけを作ってやればよい。
まずPOSTで”token”というキーが何かしら来ていなければ当然エラー。
firebase_adminというパッケージはpipでインストールしてimportしておいてやる必要がある。そしてinitialize_appしてあげるのだが、なぜかif notですでにinitializeが終わってないか判定後にしてあげないとエラーが出る。ここの理屈はわからないけどとりあえずこうしたら無難に動いた。jsonで別途Firebaseのコンソールで作る(プロジェクトの設定→アカウント→秘密鍵の生成)Service Account Keyなるものをを食わせる。
で、auth.verify_id_tokenということをしてあげると裏でfirebaseに検証をかけに行ってくれてこの辺は簡単。firebase上で持っているuidを返してくるのでこれをDjangoのusernameとして使う。Userモデルが取得できればdjango.contrib.authのlogin(request, user)というのを呼んでやればDjango側でのログイン処理ができてしまうので、それをBridgeのJavascriptへ返してあげればリダイレクトでアプリ内にお迎えができる。
Firebaseのトークン検証では、Email認証が終わっているかどうかを’email_verified’という項目で返してくれているので、メール認証が終わっていなければ、Bridgeの画面で認証を促す(前述)。
GoogleやTwitter認証の人はここは必ずTrueだったりするので、メール認証が終わっている人も含めて、ここまできたらDjangoアプリにUserを登録してあげて、パスワードは無効なものとし(set_unusable_password)、ログオンさせてあげてデフォルトのサークルボードも作ってあげて迎え入れる。

[app/views.py]
from django.contrib.auth import login
from django.contrib.auth.models import User
import firebase_admin
from firebase_admin import credentials, auth

def trytoken(request):

    id_token = request.POST.get('token')
    if(id_token is None):
        return JsonResponse({
            'dloggedin':False,'emailvalid':False,
            'vertoken':False,'message':str(e)
        })

    if not firebase_admin._apps:
        cred = credentials.Certificate(
            os.path.join('/xxxx/xxxxxx/sak.json'))
        firebase_admin.initialize_app(cred)

    try:
        decoded_token = auth.verify_id_token(id_token)
    except Exception as e:
        return JsonResponse({
            'dloggedin':False,'emailvalid':False,
            'vertoken':False,'message':str(e)
        })

    uid = decoded_token["uid"]

    user = User.objects.filter(last_name=uid).first()

    if(user is not None):
        login(request, user)
        return JsonResponse({
            'dloggedin':True,'emailvalid':True,
            'error':False
        })

    if(decoded_token['firebase']['sign_in_provider'] == 'password'):
        if(decoded_token['email_verified'] == False):
            return JsonResponse({
                'dloggedin':False,'emailvalid':False,
                'vertoken':True
            })

    # Email is verified so create user
    user = User.objects.create_user(
        username = decoded_token.get('user_id'),
        email = decoded_token.get('email'),
        first_name = decoded_token['name'],
        last_name = decoded_token['uid']
    )
    user.set_unusable_password()
    user.save()

    login(request, user)

    circle = Circle(
        name = 'Personal Board of ' + user.first_name
    )
    circle.save()

    j = Joining(
        user = user,
        circle = circle
    )
    j.save()

    return JsonResponse({
        'dloggedin':True,'emailvalid':True,
        'vertoken':True
    })

肝の部分はここまででほとんどOK。

djangoのsettings.py

Djangoのsettings.pyを少し変える必要がある。
LocaleMiddleWareは、Portalでfirebase authuiに言語を教えて上げるために追加した。
LOGOUT_REDIRECT_URLは、logout_firebaseという画面に一度飛ばして上げて、Firebaseとブラウザの関係もちょん切って上げるために。あとはGlobalLoginRequiredMiddlewareに教えてあげるための認証を必要としないパスをPUBLIC_PATHとして設定してやる。

MIDDLEWARE = [
    ......
    'django.middleware.locale.LocaleMiddleware',
    ......
]

LOGIN_REDIRECT_URL = '/app/circle_board'
LOGOUT_REDIRECT_URL = '/app/logout_firebase'

PUBLIC_PATHS = [
    '/app/portal',
    '/app/bridge',
    '/app/trytoken',
    '/app/logout_firebase',
]

logout_firebase

viewはこれだけでいい。

def logout_firebase(request):
    return render(request, 'logout_firebase.html')

HTML側で、ブラウザとFirebaseのログオン状態を切ってから、Portalにリダイレクトしてやる。
なぜかこれだけModuleでやってるので後で書き直す。window.xxxx = function () => { };みたいなことをやってグローバルに呼べる関数を作ってあげたりしないといけなくて、これほんとどうかしているよな・・・。

{% load static %}
<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <script type="text/javascript" src="{% static 'js/jquery/jquery.min.js' %}"></script>
    <title>Logout also from Firebase</title>
  </head>
  <body>

    <h1>Simple Circle Service</h1>

    <input type='hidden' value="" name="status" id="status" />

    <p id="exit" style="visibility: hidden;"><a href="{% url 'app:portal' %}">Back to Logon Screen.</a></p>

    <script type="text/javascript">
      setInterval(nnn,1000);
      function nnn(){
        logon_check();
        if($('#status').val() == 'logout'){
            $("#exit").css('visibility','visible');
        }
      }
    </script>

    <script type="module">
      import { initializeApp } from 'https://www.gstatic.com/firebasejs/9.10.0/firebase-app.js'
      import { getAnalytics } from 'https://www.gstatic.com/firebasejs/9.10.0/firebase-analytics.js'
      import { getAuth, signInWithEmailAndPassword, signOut, signInWithRedirect,
        onAuthStateChanged, GoogleAuthProvider, getRedirectResult }
       from 'https://www.gstatic.com/firebasejs/9.10.0/firebase-auth.js'
      import { getFirestore } from 'https://www.gstatic.com/firebasejs/9.10.0/firebase-firestore.js'

      const firebaseConfig = {
        apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        authDomain: "xxxxxxxxxxxx.firebaseapp.com",
        projectId: "xxxxxxxxxxxxxxxx",
        storageBucket: "xxxxxxxxxxxx.firebaseapp.com",
        messagingSenderId: "333333333333333",
        appId: "9:9999999999999:web:01234567890123456",
      };

      // Initialize Firebase
      const app = initializeApp(firebaseConfig);
      const auth = getAuth(app);
 
      window.logon_check = () => {
        console.log(auth.currentUser);
      }

      signOut(auth).then(() => {
        // Sign-out successful.
        console.log("Logout Successful.")
        window.document.getElementById("status").value = "logout";
      }).catch((error) => {
        // An error happened.
      });

    </script>
  </body>
</html>

以上で、Firebaseを使ったDjangoアプリへのログオンの仕組みができた。長い道のりだった。今後なにか公開サービスアプリを作るぞと思ったらこれで大体やれそうで嬉しい。

Google Apps Scriptが使えるかどうか調べた

自前で作っている工数/タスク管理のWebアプリケーションについて、大量のデータをアップロードする機能をカバーするためにどうするか考えたのだけど

ExcelやCSVなどのファイルアップロード → 実装がめんどくさい、Excelは無料のツールじゃない
テキストで画面上のテキストエリアに貼り付け → なんかあまり見ないしいいのかどうかわからない
フロントエンドで何かをガリガリ作る → 工数かかる

どうしたもんかと思っていて、Google Sheetsでスプレッドシート作ってマクロ付きみたいな感じで配布、誰でもそれを自分のドライブにコピーしたら、それを開いたら大量件数のデータアップロードのクライアントとして使ってもらって、裏でScriptかなんか書いて、APIでアプリに上がる仕組みにすればいいやん、と思い立って調べた。

Google Apps Scriptというのを使うということがわかって、少し調べてここまでわかった。

スプレッドシートからスクリプト実行 → できる
スクリプトでWeb APIにJSONをPOST → できる
スクリプト付きのスプレッドシートの配布 → できそうだけど使う人の権限設定とかややこしいし、なんか変な警告メッセージとかアカウントへのアクセス許可とか、仰々しいところをクリアしないと行けなくて怪しさ満点な雰囲気がする

結局、自分一人で使うものはすぐできそうだけども、動くものを公に配れるようにするのはなんか大変そうだ。使い道は色々ありそうだけど、自分の求めているものとは違う気がする。

Lubuntu で作業してたけどFirefoxが急に固まってこまってたけどSWAP足したらすぐ解決

プログラミング作業用の環境をHyper-VでLubuntuで物理メモリは2G割り当てて作って使っている。

VS Code突っ込むのはしんどかろうと思ったのでAtomエディタ+Terminal+Firefoxだけを立ち上げて、それで必要な作業は全部できるようになったので、いやいやいい時代だとやってたけど、どうしても時々Firefoxが急にハングしてしまう。だんだん重くなって固まるとかでなくて突然。

うーん、って思ってたけど、あ、スワップじゃないか、とおもったら案の定Swapがデフォルトだと512Mしかない。2GB追加してみる。

$ sudo dd if=/dev/zero of=/swapfile2 bs=1024 count=2M
2097152+0 records in
2097152+0 records out
2147483648 bytes (2.1 GB, 2.0 GiB) copied, 7.09391 s, 303 MB/s

$ sudo mkswap /swapfile2
$ sudo chmod 0600 /swapfile2

$ sudo vim /etc/fstab
[add]
/swapfile2                                swap           swap    defaults   0 0

$ sudo swapon /swapfile2

$ free -h
total        used        free      shared  buff/cache   available
Mem:           1.9Gi       1.2Gi        80Mi       209Mi       598Mi       310Mi
Swap:          2.5Gi       943Mi       1.6Gi

たったこれだけで完了。簡単かよ。しかもFirefox、超安定。幸せだ。生まれてきてよかった。

Windows スリープ復帰早すぎて

会社から貸与されているLaptop PCのThinkPadで、Windows 11で、Core-i5 1145G7で、DDR4 16GBって感じでぼちぼち世代が新しくなったんだけど、フタ閉じてる状態で多分スリープになってるはずなんだが、開けた瞬間に0.2秒くらいで復帰してスクリーン入ってるもんだから、スリープできてなかったのか?って毎回一瞬戸惑う。

何でもパフォーマンスよ過ぎりゃいいってものでもないな、、、となんか苦笑。

Grafanaを活用して自前Webアプリでダッシュボードを作る(4) – 厳しそうなので打ち切る

Grafanaを使って、自分の工数管理Webアプリのダッシュボードページにかっこいいパネルを並べる構想をしていたけど、この使いみちは厳しいということがわかったので諦める。

  • Time Seriesのデータであることが前提すぎる。Time Series以外のデータを扱う方法がこんなのとか見つかったんだけど、実際やってみてある程度マシにはなるけど、これで全部やるかとかはやってられない。
  • PanelだけEmbedで自分のアプリに流用する使い方がしたかったけど、やっぱりそんな曲がった使い方のために作られていないので、データのフィルタとか権限制御に無理がある。

やっぱりPythonでやるしかないってことでグラフに関してはSeabornを確認して使っていくことで決心。

Excelのグラフ機能って偉大だな・・・

Grafanaを活用して自前Webアプリでダッシュボードを作る(3) – Prometheusと連携

せっかくGrafanaを入れたので、色々Webを見ているとどうもサーバ監視とかに使うのが本流のツールのようなのでPrometheusとの連携をやってみる。

こちらのLinkを見てみると、Ubuntuの場合たった2行、ターミナルにaptコマンドを打ち込むだけでNode ExporterとPrometheusが導入できるそうな・・・。どれだけ簡単なんだ。

https://www.server-world.info/query?os=Ubuntu_20.04&p=prometheus&f=1

Node ExporterというのはどうもOSの各種統計情報を吐き出すツール、Prometheusはそれをためるツール、それをGrafanaでダッシュボードっぽく見やすくする、みたいな構成が多いらしい。

サーバリソースが見えるようになった。これ、今までこんな風にやったことなかったから少し感動だなあ。NextCloud + PhotoPrismの組み合わせってずっと変な処理動いてて重いような、疑ってたけど、そのへんよく見えるようになった。NextCloudのCron処理とかちょっと感覚空けて少なめにしてみた。