LambdaとReactでいいねボタンを作った
あけましておめでとうございます。今年もよろしくお願いします。
個人サイトでいいねボタンを実装するため、バックエンドにAWS LambdaとAWS DynamoDB、フロントエンドにReactを使っていいねボタンを作ってあげました。
こんな感じです。以下は実際の配置内容ではなく、Localstackでの実行内容になります。
さらにユーザは各自ログインが可能で、個別にいいねの数をカウントしています。簡易的なものですが、いいね数を保存する程度ならユーザ管理もDynamoDBでやっちゃっていいかなと。
実物はこちら。知り合いのサイトです。
概要
リポジトリ
詳しいコードは以下をご覧ください。→favoExtend
提供により非公開にしました。すみません。
https://github.com/nkte8/favo-system/tree/v2024-01-02
コード非公開に伴ってterraform部分の解説が減ってしまったので追記しています。(2024-05-19)
全体構成
- フロントエンド
- AstroでWebサイトを構築(システム外)
- Cloudflare Pagesでデプロイ(システム外)
- Reactでいいねボタンをコンポーネントとして作成
- バックエンド
- Lambda URLをエンドポイントとして使用
- AWS API Gatewayは高度なRESTが必要ないこと、コストが高くつくため不採用
- 処理サーバ: AWS Lambda
- DB: AWS DynamoDB
- Lambda URLをエンドポイントとして使用
構成はひねりもなくシンプルな内容になっています。
バックエンド側実装
私はPythonがLambda対応で書ける言語の中で一番マシな言語だったのでPythonで書いています。これから勉強される方は絶対Nodejsとかの方がいいです。動的型付けは難しすぎるし、静的型付けが使いたくて仕方ない。
Lambdaの実装について
構成
ファイル構成は以下のようになっています。
- base_api.py: DynamoDBと直接やりとりするいわゆるローレイヤー
- api.py: APIとbase_apiの間をとりもつ中間レイヤー
- lambda_function.py: APIと直接やり取りする最上位レイヤー
base_api.py
今回は他の用途でもバックエンドを使いまわしたかったためクラス化しました。
import boto3
class base_api:
def __init__(self, table_name, index_key_name, endpoint_url = None) -> None:
__table = boto3.resource('dynamodb', endpoint_url=endpoint_url)
self.table = __table.Table(table_name)
self.index_key_name = index_key_name
def table_check(self, index_value, velify_key, velify_secret):
try:
exist_data = self.table.get_item(
Key={
self.index_key_name: index_value
}
)
return (
200 if exist_data['Item'][velify_key] == velify_secret else
-401)
except KeyError:
print('E: Not found')
return -404
except Exception as e:
print('E: Unexpected error')
return -500
def table_index_exist(self, index_value, ):
try:
exist_data = self.table.get_item(
Key={
self.index_key_name: index_value
}
)
return (exist_data['Item'] != None)
except KeyError:
return False
except Exception as e:
print('E: Unexpected error')
return None
def table_read(self, index_value, request_data_key):
try:
exist_data = self.table.get_item(
Key={
self.index_key_name: index_value
}
)
return exist_data['Item'][request_data_key]
except KeyError:
print('I: Not found')
return 0
except Exception as e:
print('E: Unexpected error')
return -500
def table_push(self, index_value, data_body):
update_item = {
self.index_key_name: index_value
}
update_item.update(data_body)
try:
self.table.put_item(Item=update_item)
result = 200
except Exception as e:
result = -500
print('E: Unexpected error')
return result
boto3でDynamoDBをロードして書き込んだり読み込んだりしているだけです。継承先クラスでレスポンスを作成することを想定しているため、戻り値はシンプルに数値としていて、正常と異常状態の区別は戻り値の正負で判定という仕様にしています。
api.py
ソースコードから切り出して捕捉します。
戻り値の作成は関数を作成して実施しました。
def __r_json(self, s_code, favo_value, rmsg_value = None):
return {
'statusCode': s_code,
'body': {
self.rcode_key_name: s_code,
self.favo_key_name: favo_value,
self.rmsg_key_name: rmsg_value,
},
'isBase64Encoded': False
}
Lambdaが実行された際に返却する内容を示しています。
上記により、APIレスポンスとして以下のようなjsonを取得できます。
{
"rc": "200",
"favcount": 10,
"msg": null
}
statusCodeとrcで重複してしまっている課題
statusCode
を省いた場合、Lambdaの仕様なのかそのままの状態でAPIレスポンスが行われました。(具体的には以下のように応答された)
{
"body": {
"hoge1": "hoge",
"hoge2": "huga",
...
},
"isBase64Encoded": "False"
}
おそらくstatusCode
が設定されていないレスポンスについては、どこからがAPIレスポンスでどこまでがLambda側が吸収する必要のある設定かがわからないため、すべてをレスポンスとして扱ってしまうことが原因...だと思われます。
ちなみにLambdaが関数を実行できた時点でステータスコードは200なので、パラメータを使いたい場合はpayloadに含める必要があるようです。今回はパラメータに使いたい分をbody内に設定しています。
statusCode
とrc
で重複しちゃっていて気持ち悪いので、動作には影響はないといえどうにかしたいです。
いいね数の読み書きは以下で実施しています。
def db_fav_read(self, identify_value,
auth_key_secret = None):
if self.auth_key_name != None:
auth_result = self.__db_id_auth(
identify_value,auth_key_secret)
if auth_result < 0:
return self.__r_json(abs(auth_result),None)
value = self.table_read(
index_value=identify_value,
request_data_key=self.db_data_name
)
if value >= 0:
return self.__r_json(200,value)
else:
return self.__r_json(abs(value), None)
def db_fav_push(self, identify_value,
auth_key_secret = None):
data_body = {}
if self.auth_key_name != None:
auth_result = self.__db_id_auth(
identify_value,auth_key_secret)
if auth_result < 0:
return self.__r_json(abs(auth_result),None)
data_body.update({
self.auth_key_name: auth_key_secret
})
value = self.table_read(
index_value=identify_value,
request_data_key=self.db_data_name
)
if value < 0:
return self.__r_json(abs(value), None)
value += 1
data_body.update({
self.db_data_name: value
})
r_val = self.table_push(
index_value=identify_value,
data_body=data_body
)
if r_val < 0:
value -=1
return self.__r_json(abs(r_val),value)
authと書かれている部分はユーザが実装されている場合に動作する仕組みで、いいね処理の度にPOSTに付与されたハッシュ(パスワード)とユーザIDが一致しているかを確認しています。
いいねの加算はDBから読み込んだ値に+1して上書きを実施しているだけです。大型なサイトの場合は、同時接続時の排他処理などを考慮する必要がありますが、個人ブログならこれで十分です。
また、DBへの書き込みに失敗した場合もいいね数をレスポンスしています。これはフロントエンド側の実装を簡略化することに役立っています。
いいねとは直接関係はないユーザ認証部分は以下になります。
def __randomname(self, n):
randlst = [random.choice(string.ascii_letters + string.digits) for i in range(n)]
return ''.join(randlst)
def db_id_register(self, identify_value):
if self.auth_key_name == None:
return self.__r_json(abs(500),None)
if self.table_index_exist(index_value=identify_value):
return self.__r_json(abs(409),None)
db_data_init_value = 0
auth_key_secret = self.__randomname(20)
data_body = {
self.db_data_name: db_data_init_value,
self.auth_key_name: auth_key_secret
}
r_val = self.table_push(
index_value=identify_value,
data_body=data_body
)
return self.__r_json(abs(r_val),db_data_init_value, auth_key_secret)
def db_id_auth(self, identify_value, auth_key_secret):
if self.auth_key_name != None:
auth_result = self.__db_id_auth(
identify_value,auth_key_secret)
return self.__r_json(abs(auth_result),None)
else:
return self.__r_json(abs(-405),None)
def __db_id_auth(self, index_value, auth_key_secret):
if auth_key_secret != None:
return self.table_check(
index_value=index_value,
velify_key=self.auth_key_name,
velify_secret=auth_key_secret
)
else:
return -405
ユーザー管理もDynamoDBで実装しました。ただ、DynamoDBのダッシュボード上でKeyValueが平文で表示されてしまうためセキュリティ的にあまりよろしくありません。そのため、パスワードについてはサーバ側で生成し、これを利用してもらう方法をとりました。
base64で難読化することも考えましたが、難読化したとしても個人的に他人のパスワード情報を所持するのはちょっと荷が重いのでやめました。パスワードをなくしちゃった場合にアカウント復元ができませんが、逆に言うと個人を特定できない情報しか扱っていないため安全です。ブログでやるようなことかは謎ですが、この辺はそのうち何とかしたいですね。
lambda_function.py
lambdaが最初にハンドルする関数になります。
import json
from api import favo_api
favo_table = "favodb"
user_table = "userdb"
# endpoint_url="http://localhost:4566"
page_api = favo_api(favo_table,
favo_key_name="favcount")
user_api = favo_api(user_table,
auth_key_name="secret",
favo_key_name="favcount")
def handler(event, context):
result = {
'statusCode': 404
}
if (event.get('requestContext', {}).get('http', {}).get('method') is not None):
method = event['requestContext']['http']['method']
if method != "POST":
raise Exception("Invalid method")
body = json.loads(event.get('body',None))
arg = body.get('arg',None)
user_id = body.get('user',None)
user_pw = body.get('secret',None)
page_id = body.get('id',None)
if user_id != None:
match arg:
case "read":
result = user_api.db_fav_read(
user_id,auth_key_secret=user_pw)
case "push":
result = user_api.db_fav_push(
user_id,auth_key_secret=user_pw)
case "register":
result = user_api.db_id_register(user_id)
case "auth":
result = user_api.db_id_auth(
user_id,auth_key_secret=user_pw)
case default:
raise Exception("Invalid order")
if page_id != None:
match arg:
case "read":
result = page_api.db_fav_read(page_id)
case "push":
result = page_api.db_fav_push(page_id)
case default:
raise Exception("Invalid order")
return result
favodbとuserdbでクラスを使いまわし、別々のインスタンスを作成しています。
lambda_urlに送られた情報はevent
変数に格納されていて、これを展開・解析してメソッドを実行します。今回はREST APIではないので全部POSTに統一しました。
基本的にはインスタンスからメソッドを実行して、resultを最終的な戻り値として返却するだけの作りになっています。
AWSへの構築手段について
AWSへのデプロイはTerraformを利用しました。冪等的であるのと、楽だからです。
localstackの利用
LocalStackを用いるとかなり気軽にトライアンドエラーができるのでお勧めです。
★追記
レポジトリでタグ打ちされているlocalstackのdocker-composeの設定が古かったみたいで、現在のlocalstack(v3.0.2)では、以下のような設定で必要十分なようです。
version: "3.8"
services:
localstack:
container_name: "${LOCALSTACK_DOCKER_NAME:-localstack_main}"
image: localstack/localstack:3.0.2
ports:
- "127.0.0.1:4510-4559:4510-4559"
- "127.0.0.1:4566:4566"
environment:
- DEBUG=${DEBUG:-0}
- EXTRA_CORS_ALLOWED_ORIGINS=*
volumes:
- type: bind
source: ${TMPDIR:-/tmp/}localstack
target: /var/lib/localstack
- type: bind
source: /var/run/docker.sock
target: /var/run/docker.sock
レポジトリ上のタグ打ちされている古い&誤っている内容
※記事公開時の内容です
参考
docker-composeは以下のように設定します。
version: "3.8"
services:
localstack:
container_name: "${LOCALSTACK_DOCKER_NAME-localstack_main}"
image: localstack/localstack:3.0.2
ports:
- "127.0.0.1:53:53" # only required for Pro (DNS)
- "127.0.0.1:53:53/udp" # only required for Pro (DNS)
- "127.0.0.1:443:443" # only required for Pro (LocalStack HTTPS Edge Proxy)
- "127.0.0.1:4510-4559:4510-4559" # external service port range
- "127.0.0.1:4566:4566" # LocalStack Edge Proxyll
environment:
- DEBUG=${DEBUG-}
- DATA_DIR=${DATA_DIR-}
- LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR-}
- LOCALSTACK_API_KEY=${LOCALSTACK_API_KEY-} # only required for Pro
- HOST_TMP_FOLDER=${TMPDIR:-/tmp/}localstack
- DOCKER_HOST=unix:///var/run/docker.sock
- EXTRA_CORS_ALLOWED_ORIGINS=*
volumes:
# - "${TMPDIR:-/tmp}/localstack:/tmp/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
なぜかHyper-V上のUbuntuでDocker composeが/tmpディレクトリをマウントできなかったためコメントアウトしています。コンテナ再起動の際に環境が消えてしまうデメリットを抱えてしまうことになりましたが、デプロイの確認がしたい程度だったので許容しました。
そもそもHOST_TMP_FOLDER
はLambda volume mountをする際しか使われないとのことで、コメントアウトして平気な設定でした。
ローカル環境と本番環境の使い分け
★追記
Terraformのmoduleの機能を使ってtfvarsを使い分けすることができました。
現在のリポジトリでは以下のようなディレクトリ構成になっています。
local
\ .terraform.lock.hcl
\ main.tf <------------- moduleの読み込みを定義
\ output.tf
\ provider.tf <---------- backendをlocalに指定
\ terraform.tfvars
\ variable.tf
modules
\ dynamodb <------------- 個別のresourceを定義
\ ....tf
\ lambda_function
\ ....tf
prod
\ .terraform.lock.hcl
\ main.tf <------------- moduleの読み込みを定義
\ output.tf
\ provider.tf <---------- backendをs3に指定
\ terraform.tfvars
\ variable.tf
main.tfは以下のように設定します。module内で使用する変数をmain.tfのmoduleブロックから設定を行うことで、module内に読み込みを行うことができます。
module "dynamodb" {
source = "../modules/dynamodb"
env = var.env
dynamodb_props = var.dynamodb_props
dynamodb_no_import_props = var.dynamodb_no_import_props
}
module "lambda" {
source = "../modules/lambda_function"
iam_lambda = var.iam_lambda
lambda_favo_counter_payload_name = var.lambda_favo_counter_payload_name
lambda_favo_counter_payload_src = var.lambda_favo_counter_payload_src
lambda_favo_counter_name = var.lambda_favo_counter_name
lambda_favo_counter_handler = var.lambda_favo_counter_handler
lambda_favo_counter_function_url_cors_origin = var.lambda_favo_counter_function_url_cors_origin
lambda_layer_payload_name = var.lambda_layer_payload_name
lambda_layer_payload_src = var.lambda_layer_payload_src
lambda_layer_name = var.lambda_layer_name
}
module機能を知る前の内容
localstackと本番環境への環境の切り替えは面倒ですが手動で行っています。
本番環境のtfstateはs3で管理したかったのですが、localstack側はlocalで十分だという考えからです。
# provider.tf
variable "aws_region" {}
provider "aws" {
region = var.aws_region
}
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "favo-system"
region = "ap-northeast-1"
key = "terraform.tfstate"
encrypt = true
}
# backend "local" {
# path = "tflocal.tfstate"
# }
}
lambdaのTerraform構築
リポジトリを非公開にしてしまったので、こちらで一部追記しておきます。
lambdaへ関数を構築するコードはこんな感じです。
# lambda.tf
data "archive_file" "lambda_favo_counter_payload" {
type = "zip"
source_dir = var.lambda_favo_counter_payload_src
output_path = var.lambda_favo_counter_payload_name
}
resource "aws_lambda_function" "lambda_favo_counter_function" {
filename = var.lambda_favo_counter_payload_name
function_name = var.lambda_favo_counter_name
role = aws_iam_role.iam_lambda.arn
handler = var.lambda_favo_counter_handler
source_code_hash = data.archive_file.lambda_favo_counter_payload.output_base64sha256
runtime = "python3.11"
layers = [
aws_lambda_layer_version.lambda_layer.arn
]
}
data "archive_file" "lambda_layer_archive" {
type = "zip"
source_dir = var.lambda_layer_payload_src
output_path = var.lambda_layer_payload_name
}
resource "aws_lambda_layer_version" "lambda_layer" {
filename = var.lambda_layer_payload_name
layer_name = var.lambda_layer_name
compatible_runtimes = ["python3.11"]
}
...(中略)...
同じディレクトリにてiamリソースも作成しています。面倒くさかったので、既存のRole設定を紐づけているだけですが。そんなかんじ。
# iam.tf
data "aws_iam_policy_document" "assume_role" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
}
resource "aws_iam_role_policy_attachment" "dynamodb_access" {
role = aws_iam_role.iam_lambda.id
policy_arn = "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess"
}
resource "aws_iam_role" "iam_lambda" {
name = var.iam_lambda
assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
Lambda URLリソース自体はTerraformでデプロイ可能ですが、デプロイ後にURLがわからないと不便なため、outputにより出力をしています。
...(中略)...
resource "aws_lambda_function_url" "lambda_favo_counter_function_url" {
function_name = aws_lambda_function.lambda_favo_counter_function.arn
authorization_type = "NONE"
cors {
allow_credentials = true
allow_origins = var.lambda_favo_counter_function_url_cors_origin
allow_methods = ["POST"]
allow_headers = ["content-type"]
expose_headers = []
max_age = 86400
}
depends_on = [
aws_lambda_function.lambda_favo_counter_function
]
}
output "lambda_favo_counter_function_url" {
value = aws_lambda_function_url.lambda_favo_counter_function_url.function_url
}
outputはlocalstackの場合も実施され、以下のようなlocalhost向けのアドレスがきちんと出力されます。
http://o23gmw2qx8e5vxz23wgk7m9u79173h18.lambda-url.ap-northeast-1.
localhost.localstack.cloud:4566/
dynamodbのterraform構築
DBについては前にも書いたようにファボ用とユーザ用で分けていますが、コード上では別の捉え方をしていました。
「既存のDBバックアップから復元するか否か」という基準でコードが分かれています。
# dynamodb.tf
resource "aws_dynamodb_table" "dynamodb" {
for_each = var.dynamodb_props
name = each.value.name
billing_mode = "PROVISIONED"
read_capacity = each.value.read_capacity
write_capacity = each.value.write_capacity
hash_key = each.value.hash_key
attribute {
name = each.value.hash_key
type = "S"
}
point_in_time_recovery {
enabled = true
}
deletion_protection_enabled = true
import_table {
input_compression_type = "GZIP"
input_format = "DYNAMODB_JSON"
s3_bucket_source {
bucket = each.value.import_s3_bucket
key_prefix = each.value.import_s3_key_prefix
}
}
}
resource "aws_dynamodb_table" "dynamodb_no_import" {
for_each = var.dynamodb_no_import_props
name = each.value.name
billing_mode = "PROVISIONED"
read_capacity = each.value.read_capacity
write_capacity = each.value.write_capacity
hash_key = each.value.hash_key
attribute {
name = each.value.hash_key
type = "S"
}
point_in_time_recovery {
enabled = true
}
deletion_protection_enabled = true
}
dynamodb_no_import_props
を使った場合は新規で作成、dynamodb_props
を使った場合は、既存のS3バケットからバックアップをimportして新規作成します。
変数は以下のような感じで設定していました。
...(中略)...
dynamodb_no_import_props = {}
dynamodb_props = {
favodb = {
name = "favodb"
read_capacity = 5
write_capacity = 5
hash_key = "Identify"
import_s3_bucket = "favo-system"
import_s3_key_prefix = "AWSDynamoDB/01704264010692-bf533c21/data/"
},
userdb = {
name = "userdb"
read_capacity = 5
write_capacity = 5
hash_key = "Identify"
import_s3_bucket = "favo-system"
import_s3_key_prefix = "AWSDynamoDB/01704264023314-39b2fc15/data/"
},
}
このコードを書いた頃、terraform力が低かったのであまりいい書き方ではないです。
※favoExtend
では、Terraformコードに関しても改善されています。
フロントエンド側実装
最近勉強していて、Reactの便利さを理解し始めました。Typescriptは型付けが静的で素晴らしい、そのうちバックエンドもTypescript/Nodejsに置き換えたいと思っています。
Reactの実装について
バックエンドへのアクセスはfetchモジュールを用いました。また、これを実行するフロントエンドとしてのコンポーネントを開発しました。
本文章では以下三つについて説明します。
- favoapi.ts: APIアクセス用モジュール
- favobutton: いいねボタン本体
- userform: ユーザ登録フォーム
favoapi.ts について
ここではlocalstorageなどには触れず、API部分のみ補足します。
type ApiResponse = {
rc: number;
favcount: number | null;
msg: string | null;
}
export const favo_api = async (
api_url: string,
id: string | null,
userid: string | null,
secret: string | null,
arg: "read" | "auth" | "register" | "push",
): Promise<ApiResponse> =>
fetch(api_url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(
{
id: id,
user: userid,
secret: secret,
arg: arg,
})
}
).then((response) => response.json()
).catch(() => {
console.log("error");
})
fetchモジュールは非同期的にインターネットへアクセスを行うモジュールです。fetchをそのまま利用するとPromise<any>
となってしまいます。
これでは使いにくいため、favo_api
関数はfavo_api
はfetchモジュールのrapperとして実装しました。これによりPromise<ApiResponse>
型として返却されます。
favobuttonコンポーネント
機能部について
import { useState, useEffect } from 'react';
import './main.css';
import { favo_api, get_auth_local } from '@/utils/favoapi'
interface Props {
api_url: string,
page_name: string,
}
export default function Favobutton({ api_url, page_name }: Props) {
const auth = get_auth_local()
const [count, setCount] = useState(0);
const [active, setActive] = useState(false);
const runFavoApi = async (arg: "read" | "push") => {
try {
let c = await favo_api(api_url, page_name, auth.id, auth.secret, arg)
if (c.favcount !== null) {
setCount(c.favcount)
}
return (c.favcount !== null)
} catch (e) {
return false
}
}
useEffect(() => {
// 非同期処理の場合は、関数を定義しそれを呼び出すような形式で記述すること
runFavoApi("read")
}, [])
const handleClick = async () => {
if (await runFavoApi("push")) {
setActive(true);
}
};
return (
<button className={`favobutton ${active ? "clicked" : ""}`} onClick={handleClick}>
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 15">
<path d="M4 1c2.21 0 4 1.755 4 3.92C8 2.755 9.79 1 12 1s4 1.755 4 3.92c0 3.263-3.234 4.414-7.608 9.608a.513.513 0 0 1-.784 0C3.234 9.334 0 8.183 0 4.92 0 2.755 1.79 1 4 1z" />
</svg>
{count}
</button>
);
};
tsx
はReactコンポーネントで、戻り値に記載された内容をそのまま呼び出し元に利用することができます。
非同期的に処理を行うためにuseEffect
を用いています。初回のいいね数読み込みはこれにより解決していて、タップ/クリックによるカウントの増加についてはAPIからのレスポンスを利用しています。
DBへの書き込みに失敗した場合もいいね数をレスポンスしています。これはフロントエンド側の実装を簡略化することに役立っています。
この仕様により、DBへの書き込みの可否によらず、フロントエンド側はレスポンスを表示するだけで機能します。
ボタンのアニメーションについて
ボタンのアニメーションはCSSで行っています。
button.favobutton {
...(中略)...
transition: 0.05s linear;
vertical-align: middle;
}
button:active {
transform: scale(0.90);
}
button.clicked {
color: #e44;
}
...(中略)...
transformで形状変化させているだけです。クリック後赤い状態になるのはReact側でcliked
クラスを付与して対応しています。
このclassは動的なものなので、ページをリロードすると元に戻りますが仕様です。ボタンは何度でも押せるようにしているので、状態について気にする必要がないためです。
userformコンポーネント
機能部について
長いのでかいつまんで解説します。
...(中略)...
const userid_pattern = "^([a-zA-Z0-9]{4,10})$"
const secret_pattern = "^([a-zA-Z0-9]{20,20})$"
const onClickLogin = async () => {
if (userid.match(userid_pattern)) {
if (arg === "auth" && !secret.match(secret_pattern)) {
setMsg("IDは4~10文字の半角英数で指定してください")
return
}
try {
let result = await favo_api(api_url, null, userid, secret, arg);
switch (result.rc) {
case 200:
let sec_local = arg === "register" ? result.msg : secret
if (sec_local == null) {
setMsg("不明なサーバーエラーが発生しました。");
return
}
set_auth_local({ id: userid, secret: sec_local })
window.location.href = login_path + "?login";
break;
case 401:
setMsg("ID/PWの組み合わせが誤っています。");
break;
case 409:
setMsg("すでに使用されているIDです。");
break;
case 500:
setMsg("不明なサーバーエラーが発生しました。");
break;
}
} catch (e) {
setMsg("不明なサーバーエラーが発生しました。")
}
}
}
...(中略)...
上記はログインまたはユーザ登録時に実行されるコードです。レスポンスのrc
値を見て処理を判断させています。
...(中略)...
const checkUserByApi = async () => {
if (userid !== null && secret !== null) {
try {
let r = await favo_api(api_url, null, userid, secret, "auth")
if (r.rc === 200) {
setLoginStatus(true)
} else {
rm_auth_local()
setMsg("再ログインをお願いします。");
}
} catch (e) {
setMsg("不明なサーバーエラーが発生しました。");
}
}
}
useEffect(() => {
checkUserByApi()
}, [])
...(中略)...
例によってロード時にログインが可能かをuseEffect
を用いて確認しています。不可能な場合はログアウトの処理(localstorageからkeyvalueを削除する処理)を行っています。
...(中略)...
return (
<div className="userform">
{
islogin !== true &&
<div className="context">
{form_msg}
</div>
}
{
islogin === true &&
<React.StrictMode>
<div className="context">
{userid}はログイン中です。
</div>
<div className="context">
再ログインする場合は以下のパスワードをご利用ください。
</div>
<div className="context smaller">
パスワードの再発行はできません。ブラウザへの保存をお願いします。
</div>
</React.StrictMode>
}
{
islogin !== true &&
<React.StrictMode>
{msg !== null &&
<span className='context alert'>
{msg}
</span>
}
<div className='component'>
<label>ID:</label><input
type='text'
className='textbox'
pattern={userid_pattern}
value={userid}
onKeyDown={handleKeyDown}
onChange={(event) => setUserid(event.target.value)}
/>
</div>
</React.StrictMode>
}
{
(arg === "auth" || islogin === true) &&
<div className='component'>
<label>PW:</label><input
type={isOpenpw}
className='textbox'
pattern={secret_pattern}
value={secret}
onFocus={() => setOpenPW("text")}
onBlur={() => setOpenPW("password")}
onKeyDown={handleKeyDown}
onChange={(event) => setSecret(event.target.value)}
/>
</div>
}
{
islogin !== true &&
<React.StrictMode>
<div className='component'>
<button className='submit' onClick={onClickLogin}>
{button_msg}
</button>
</div>
</React.StrictMode>
}
{
islogin === true &&
<React.StrictMode>
<div className='component'>
<button className='submit' onClick={onClickLogout}>
Logout
</button>
</div>
</React.StrictMode>
}
</div>
);
};
かなりごちゃごちゃ書いていてわかりにくいですが、主にフォームが登録用かログイン用かを示す式arg === "register" | "auth"
、ログイン中かの状態を記録している変数islogin
を用いて判定しています。
登録とログインで同じフォームを用いているため上記のような実装を行っています。
不適切な値表示について
フォームに不適切な値、具体的には機能部で指定した正規表現にマッチしない場合に表示するようになっています。
これについてはCSSで表現可能です。
...(中略)...
.userform input[type=text]:invalid,
input[type=password]:invalid {
background: pink;
}
...(中略)...
あくまで表現が変化するだけで、入力を受け付けない等の制御はできません。これについてはReact側で対応しています。
Astroへの実装について
AstroはReactを公式でサポートしており、インテグレーションとして導入が可能です。
...(中略)...
import react from "@astrojs/react";
// https://astro.build/config
export default defineConfig({
...
integrations: [
...
, react()
]
});
Astro側の実装としても、通常のコンポーネントと同様に呼び出してあげれば使用可能です。
---
...(中略)...
import Favobutton from "@/components/favobutton/main.tsx";
const api_url = lambdaurl;
---
<div class="pagerefbuttons">
<div class="left">
{nextlink !== null && <a href={nextlink.href}>{nextlink.msg}</a>}
</div>
<div class="center">
<Favobutton
client:load
api_url={api_url}
page_name={currentpage_slug}
/>
</div>
<div class="right">
{prevlink !== null && <a href={prevlink.href}>{prevlink.msg}</a>}
</div>
</div>
参考
Discussion