トップへ(mam-mam.net/)

PHP標準セッション+MySQLでWEBシステム開発 | ログイン,ログアウト,ユーザー管理

PHP標準セッション+MySQLでWEBシステム開発 | ログイン,ログアウト,ユーザー管理

素のPHPと標準セッション+MySQLで動作するWEBシステムの基礎開発を行います。
こちら(PHP+MySQLでWEBシステム開発)では独自のセッション管理開発を行いますが、本記事ではPHPの標準セッションを使用してログイン、ログアウト、ユーザー管理システムを開発します。
PHPの標準セッションについてはこちら(PHPの標準セッションについて)をご参照ください。
PHPの標準セッションで開発する場合は、独自セッションと違い、1組のIDとパスワードを別端末で同時使用してログインできます。
開発するのはログイン、ログアウト、ユーザー管理、パスワード変更のみ、MVCアーキテクチャ寄せで開発します。
(JWTはサーバーで属性を保持できない場合に使用するものでセキュリティも緩くなるので使いません。)
データベースはローカルにインストールされたMySQL又はMariaDBを使用します。
ルーティングなど不要なものは作りません。(コントローラーの ディレクトリ名/ファイル名 がURLそのものであり圧倒的にわかりやすい。)
また、最近はPHP製WEBシステムが殆どなので拡張子を隠す意味がないので拡張子は隠しません。
DB接続パスワードなどは適宜変更してお使いください。

ディレクトリ構造、各ファイルの配置場所

共通ファイル
_inc/config.php定数定義、共通関数
_inc/head.php<head></head>内に入れる共通ヘッダ
モデル
_model/機能名.php
_model/db.phpモデルの基底クラス(DB接続、SQL実行などを実装)
_model/change_password.phpパスワード変更 モデル
_model/login.phpログイン モデル
_model/logout.phpログアウト モデル
_model/manage_user.phpユーザー管理 モデル
_model/menu.phpメニュー モデル
ビュー
_view/機能名/xxx.php
_view/change_password/index.phpパスワード変更(入力画面)
_view/change_password/confirm.phpパスワード変更(確認画面)
_view/change_password/complete.phpパスワード変更(完了画面)
_view/login/index.phpログイン画面
_view/logout/index.phpログアウト画面
_view/manage_user/index.phpユーザー管理(一覧表示画面)
_view/manage_user/input.phpユーザー管理(追加・変更画面)
_view/manage_user/confirm.phpユーザー管理(追加・変更確認画面)
_view/manage_user/complete.phpユーザー管理(追加・変更完了画面)
_view/menu/index.phpメニュー画面
コントローラー
機能名/xxx.php
change_password/index.phpパスワード変更入力
change_password/confirm.phpパスワード変更確認
change_password/regist.phpパスワード変更処理
change_password/complete.phpパスワード変更完了
login/index.phpログイン
login/tologin.phpログイン処理
logout/index.phpログアウト
manage_user/index.phpユーザー管理(一覧表示)
manage_user/input.phpユーザー管理(追加・変更)
manage_user/confirm.phpユーザー管理(追加・変更確認)
manage_user/regist.phpユーザー管理(追加・変更確認)処理
manage_user/complete.phpユーザー管理(追加・変更完了)
menu/index.phpメニュー
css
css/common.css
リダイレクトファイル
index.php

データベースとテーブル、ユーザーの作成

データベースはローカルにインストールされたMySQL又はMariaDBを使用するものとします。
以下をPHPMyAdminなどで実行させてデータベースとテーブルを作成します。

初期値設定は、管理者が ログインID「admin」、パスワード「admin」でログインできるように設定します。
パスワードハッシュはPHPの password_hash("admin",PASSWORD_DEFAULT) 関数にて取得した パスワード「admin」のハッシュ値です。

-- データベース db_mytest_session の作成
CREATE DATABASE IF NOT EXISTS db_mytest_session CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- ユーザー「user」をパスワード「Pass#0123456」で作成する
CREATE USER IF NOT EXISTS 'user'@'localhost' identified by 'Pass#0123456';
-- ユーザー「user」に データベース「db_mytest_session」に対するすべての権限を付与する
GRANT all ON db_mytest_session.* to 'user'@'localhost';
-- データベース「db_mytest_session」に切り替える
USE db_mytest_session;

-- ユーザーテーブルのパスワードハッシュのみ照合順序をバイナリにして大文字小文字を区別する
CREATE TABLE IF NOT EXISTS tm_user(
  user_id SERIAL PRIMARY KEY COMMENT 'ユーザーID',
  user_name VARCHAR(100) COMMENT 'ユーザーName',
  user_login_id VARCHAR(100) NOT NULL COMMENT 'ログインID',
  user_login_hash VARCHAR(255) NOT NULL COLLATE 'utf8mb4_bin' COMMENT 'パスワードハッシュ',
  user_login_hash_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'パスワード変更日',
  user_permission INT NOT NULL DEFAULT 0 COMMENT '権限',
  user_disabled INT NOT NULL DEFAULT 0 COMMENT '0:使用可能 1:使用不可',
  user_login_date DATETIME NOT NULL DEFAULT '2000-01-01' COMMENT 'ログインした日時',
  user_fail_date DATETIME NOT NULL DEFAULT '2000-01-01' COMMENT 'ログイン失敗した日時',
  user_fail_count INT NOT NULL DEFAULT 0 COMMENT 'ログイン失敗した回数',
  user_upid BIGINT NOT NULL DEFAULT 0,
  user_update DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  user_regid BIGINT NOT NULL DEFAULT 0,
  user_regdate DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE INDEX idx_user_lgoin_id(user_login_id)
);

-- ユーザー「admin」、パスワード「admin」のユーザーを登録
INSERT INTO tm_user(
  user_name, user_login_id,
  user_login_hash, user_login_hash_date,
  user_permission, user_disabled,
  user_upid, user_regid
) VALUES(
  '管理者', 'admin',
  '$2y$10$EteTmtC.pOIwUkr944K/deZuLl5KOo3Bneu44CADQVryXUiFdT2MK', '2026-08-05',
  1, 0,
  0, 0
);

CREATE TABLE IF NOT EXISTS tm_permission(
  perm_permission INT NOT NULL DEFAULT 0 COMMENT '権限',
  perm_permission_mean VARCHAR(100) DEFAULT NULL COMMENT '権限名'
);

INSERT INTO tm_permission(perm_permission, perm_permission_mean) VALUES
(0, '選択してください'),
(1, '管理者'),
(2, '一般');


CREATE TABLE tm_disabled(
  disa_disabled int NOT NULL DEFAULT 0 COMMENT '使用可否',
  disa_disabled_mean varchar(100) DEFAULT NULL COMMENT '意味'
);

INSERT INTO tm_disabled(disa_disabled, disa_disabled_mean) VALUES
(0, '使用可能'),
(1, '使用不可');



db_mytest.tm_user
カラム名その他備考
user_idBIGINT UNSIGNED NOT NULL AUTO_INCREMENTプライマリキーユーザーID
user_nameVARCHAR(100)ユーザー名
user_login_idVARCHAR(100) NOT NULLユニークインデックスログインID
user_hashVARCHAR(255) NOT NULLutf8mb4_binパスワードハッシュ
user_login_hash_dateDATETIME NOT NULL DEFAULT CURRENT_TIMESTAMPパスワード変更日
user_permissionINT NOT NULL DEFAULT 0権限
user_disabledINT NOT NULL DEFAULT 10:使用可能 1:使用不可
user_login_dateDATETIME NOT NULL DEFAULT '2000-01-01'最終ログイン日時
user_fail_dateDATETIME NOT NULL DEFAULT '2000-01-01'ログイン失敗日時
user_fail_countINT NOT NULL DEFAULT 0ログイン失敗回数
user_upidBIGINT NOT NULL DEFAULT 0更新者
user_updateDATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP更新日時
user_regidBIGINT NOT NULL DEFAULT 0登録者
user_regdateDATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP登録日時

共通系ファイル

共通系ファイルなどはパスワードも入れる場合があるので外部から見られないようにする必要があります。
例えば外部から見られないようにするファイルはソースコード冒頭に以下のような工夫を行います。

<?php
// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
・・・
__FILE__
インクルードされたファイルの絶対パス(フルパス と ファイル名)
$_SERVER['SCRIPT_FILENAME']
インクルード元ファイルの絶対パス(フルパス と ファイル名)
realpath(パス)
パスのシンボリックリンクを全展開し、 ./ や ../ などの参照をすべて解決した絶対パスを返す

呼び出し元(例えばコントローラPHPファイル)は共通ファイル(config.phpなど)を読み込みます。

<?php
  require_once __DIR__.'/_inc/config.php';

_inc/config.php

<?php
/* 共通設定、共通関数を記述するファイル */

// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}

//タイムゾーンの設定
date_default_timezone_set("Asia/Tokyo");

//デフォルトの内部文字エンコーディングをUTF-8に変更
mb_internal_encoding("UTF-8");

/* 共通定数定義 */
//タイトル(初期設定)
define("TITLE","標準セッションでログイン機能開発");

//DB接続
define("DSN","mysql:dbname=db_mytest_session;host=localhost");
define("DBUSER","user");
define("DBPASS","Pass#0123456");

//アカウントロックアウト秒
define("LOCKOUT_SECOND",900);
//ロックアウト連続失敗回数
define("LOCKOUT_COUNT",5);


//セッション最大有効期間1時間(この期間内はログイン継続とする)
define("SESSION_MAX_EXPIRE",60*60*1);

//30日でパスワード変更
define("MUST_CHANGE_PASSWORD",30);

//HTTPS通信しているか
define('IS_SECURE', isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on');

//パスワードポリシーチェック関数
function checkpass($pass){
  $flag=false;
  $err="";
  //パスワードのポリシー 0-9 a-z A-Z の文字のみで8~20文字
  if(!preg_match('/^[0-9a-zA-Z\/\.\@\!\?\#\$\%\&\=\_\|\+\*\(\)\{\}\~\<\>\;\\\:\[\]\-]{8,20}$/', $pass)){
    [$flag, $err] = [true, "半角英数記号含む8~20文字で入力してください"];
  }
  if( !$flag && !(preg_match('/[0-9]/',$pass) && preg_match('/[a-zA-Z]/',$pass) && preg_match('/[\/\.\@\!\?\#\$\%\&\=\_\|\+\*\(\)\{\}\~\<\>\;\\\:\[\]\-]/',$pass)) ){
    [$flag, $err] = [true, "英字と数字と記号を混在させてください"];
  }
  return [$flag, $err];
}
//パスワードチェック関数
function check2pass($pass1, $pass2){
  if($pass1 === $pass2){
    return [false, ""];
  }else{
    return [true, "同一パスワードを入力してください"];
  }
}


//セッションの開始
ini_set('session.gc_maxlifetime', SESSION_MAX_EXPIRE);
session_start();

/* PHP標準セッションでキャッシュを有効にする */
header("Expires: -1");
header("Cache-Control:");
header("Pragma:");

_inc/head.php

<?php
/* 共通ファイル  <head>~</head>内に共通で記述する<mata>タグや<script>タグを入れる */

// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
?>
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <meta http-equiv="content-language" content="ja">
  <meta charset="UTF-8">
  <link rel="stylesheet" href="../css/common.css">
  <title><?=TITLE?></title>

DB接続(_model)

MVCのM(モデル)の基底クラスとなる dbクラスのファイル(_model/db.php)を作ります。
POST処理はC(コントローラ)に入れるべきですがM(モデル)に入れています。
業務処理のほとんどをデータベースが担うのでPHPはDB接続を行うことになります。

_model/db.php

<?php
/* 基底 モデル */

// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
require_once(__DIR__.'/../_inc/config.php');

class db{
    protected $pdo;  //PDO接続オブジェクト
    public $post;    //POST値を入れる連想配列(コンストラクタで取得)
    public $posten;  //POST値を入れる連想配列(HTMLエンティティ化後)
    public $user_id;                         //ユーザーID(連番)
    public $user_name;                       //ユーザー名
    public $user_permission;                 //ユーザー権限
    public $user_need_change_password=false; //パスワードを変更すべきか
    public $csrf;

  //★コンストラクタ
  public function __construct(){
    //DB接続
    $this->pdo=new PDO(DSN, DBUSER, DBPASS, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
    $this->post = $_POST;
    $this->posten = $this->escapeRecursive($this->post);
  }

  //汎用 与えられたSQL文($sql)とパラメータ($param)を実行し結果を返す
  //戻り値:[行番号]["カラム名"]
  protected function execSQL($sql, $param=[]){
    $stmt = $this->pdo->prepare($sql);
    $stmt->execute($param);
    // SELECT文などの結果セットがある場合はフェッチして返す
    // INSERT/UPDATE/DELETEなどの場合は空配列が返る
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
  }


  //HTMLエンティティ化して返す
  protected function escapeRecursive($value){
      if(is_array($value)){
          $escaped = [];
          foreach ($value as $k => $v) {
              $escaped[$k] = $this->escapeRecursive($v);
          }
          return $escaped;
      }
      return is_string($value) ? htmlspecialchars($value, ENT_QUOTES, "UTF-8") : $value;
  }

  //セッションが有効か確認する
  public function isValidSession(){
    if (empty($_SESSION['user_id'])){
      return false;
    }
    if (empty($_SESSION['last_access'])) {
      $_SESSION['last_access'] = time();
    }
    if (time() - $_SESSION['last_access'] > SESSION_MAX_EXPIRE) {
      return false;
    }
    $_SESSION['last_access'] = time();
    $sql="SELECT user_id,user_name,user_permission, user_login_hash_date, DATE_FORMAT(NOW(),'%Y-%m-%d %T') nw FROM tm_user WHERE user_disabled=0 AND user_id=?";
    $ret=$this->execSQL($sql, [$_SESSION['user_id']]);
    if(empty($ret)){
      return false;
    }
    $this->user_id         = $_SESSION['user_id'];
    $this->user_name       = htmlspecialchars($ret[0]["user_name"], ENT_QUOTES, "UTF-8");
    $this->user_permission = $ret[0]["user_permission"];
    $user_login_hash_date  = strtotime($ret[0]["user_login_hash_date"]); //UNIXTIME
    //セッションの有効期限がSESSION_MAX_EXPIRE以前か確認
    $nw=strtotime($ret[0]["nw"]);//DBサーバーの現在日時
    //パスワードを直ぐに変更すべきか確認する
    if( ($user_login_hash_date+60*60*24*MUST_CHANGE_PASSWORD)<$nw ){
      $this->user_need_change_password=true;
    }else{
      $this->user_need_change_password=false;
    }
    return true;
  }


  //CSRF対策用の乱数生成
  public function createCSRF(){
    //フィールド変数 $this->csrf に入れる
    $this->csrf=bin2hex(random_bytes(32));
    //セッション変数 $_SESSION["csrf"]にも入れる
    $_SESSION["csrf"]=$this->csrf;
  }

  public function equalCSRF(){
    if(!isset($_POST['csrf']) || !isset($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $_POST['csrf'])) {
      return false;
    }else{
      return true;
    }
  }


}

_model/login.php

dbクラスを継承したログイン モデルです。
認証が成功すると クッキー「myssid」にセッションを入れます。
クッキーには「HTTP Only」属性を入れてXSS攻撃に強くしています。
(jwtはローカルストレージに保存することが多くXSS攻撃に弱くなりがちです。)

<?php
/* ログイン モデル */

// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
require(__DIR__.'/db.php');

class login extends db{
  public $err=[
    "login_err"=>"",
  ];

  public function __construct(){
    //親コンストラクタを呼ぶ
    parent::__construct();
    //POST値の整形取得
    $this->post["user_login_id"] = $this->post["user_login_id"] ?? "";
    $this->post["user_password"] = $this->post["user_password"] ?? "";
    $this->posten = $this->escapeRecursive($this->post);
  }

  //ログイン可能か調べる
  //ログインIDが存在していて失敗した場合は「fail_count++」「fail_date=NOW()」にする
  //ログイン成功した場合は「fail_count=0」にし、重複しないセッションIDを生成してクッキーに設定する
  //戻り値 成功時:ユーザーID 失敗時:false
  public function canLogin(){
    $sql="SELECT user_id,user_login_hash,user_fail_date,user_fail_count,DATE_FORMAT(NOW(),'%Y-%m-%d %T') nw FROM tm_user WHERE user_disabled=0 AND user_login_id=?";
    $ret=$this->execSQL($sql, [$this->post["user_login_id"]] );

    if(empty($ret)){
      //■ログインIDが存在しない場合
      $this->err["login_err"]="ログイン失敗";
      password_verify($this->post["user_password"], '$2y$10$e0MYzXyjpJS7Pd0RVvHwHe1fS3z4yC.X/Jq7G0x4qX1qM8j0yJ8WO');
      return false;
    }else{
      //■ログインIDが存在した場合
      $user_fail_date=strtotime($ret[0]["user_fail_date"]);
      $user_fail_count=$ret[0]["user_fail_count"];
      $nw=strtotime($ret[0]["nw"]);//DBサーバーの現在日時

      if( $user_fail_count>=LOCKOUT_COUNT && $user_fail_date>($nw-LOCKOUT_SECOND) ){
        //ログイン失敗回数が LOCKOUT_COUNT 回以上で失敗日時が LOCKOUT_SECOND 秒以内の場合
        $this->err["login_err"]="エラー回数が".LOCKOUT_COUNT."回を超えたので、このアカウントはロックアウトされました";
        return false;
      }
      //パスワードとログインハッシュからログイン検証
      $verify=password_verify($this->post["user_password"],$ret[0]["user_login_hash"]);
      if(!$verify){
        //■ログイン失敗の場合
        if($user_fail_date>($nw-LOCKOUT_SECOND)){
          //失敗日時がLOCKOUT_SECOND秒以内の場合には、失敗カウント+1と失敗日時の登録
          $sql="UPDATE tm_user SET user_fail_date=NOW(),user_fail_count=user_fail_count+1 WHERE user_id=?";
          $param=[$ret[0]["user_id"]];
          $this->execSQL($sql,$param);
          $this->err["login_err"]="ログイン失敗";
          return false;
        }else{
          //失敗日時が LOCKOUT_SECOND 秒以上前の場合には、失敗カウント=1にリセットして失敗日時を登録
          $sql="UPDATE tm_user SET user_fail_date=NOW(),user_fail_count=1 WHERE user_id=?";
          $param=[$ret[0]["user_id"]];
          $this->execSQL($sql,$param);
          $this->err["login_err"]="ログイン失敗";
          return false;
        }
      }else{
        //■ログイン成功の場合
        session_regenerate_id(true);
        //ハッシュの更新が必要か確認
        if(password_needs_rehash($ret[0]["user_login_hash"], PASSWORD_DEFAULT)) {
          $newHash = password_hash($this->post["user_password"], PASSWORD_DEFAULT);
          $sql="UPDATE tm_user SET user_login_hash=? WHERE user_id=?";
          $param=[$newHash,$ret[0]["user_id"]];
          $this->execSQL($sql,$param);
        }
        //失敗カウントを0に設定し、ログイン失敗日時を初期値設定する
        $sql="UPDATE tm_user SET user_fail_date='2000-01-01',user_fail_count=0,user_login_date=NOW() WHERE user_id=?";
        $param=[$ret[0]["user_id"]];
        $this->execSQL($sql,$param);
        $_SESSION['user_id']=$ret[0]["user_id"];
        return $ret[0]["user_id"];
      }
    }
  }
}

_model/logout.php

dbクラスを継承したログアウト モデルです。

<?php
/* ログアウト モデル */

// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
require(__DIR__.'/db.php');

class logout extends db{
  //ログアウト処理
  public function logout(){
    //セッションの破棄
    $_SESSION=[];
    session_destroy();
    if(ini_get("session.use_cookies")) {
      $params = session_get_cookie_params();
      setcookie(session_name(), '', [
        'expires'  => 1,
        'path'     => $params["path"],
        'domain'   => $params["domain"],
        'secure'   => $params["secure"],
        'httponly' => $params["httponly"],
        'samesite' => $params["samesite"] ?? 'Strict']);
    }
  }
}

_model/menu.php

dbクラスを継承したメニュー モデルです。
メニューは各種処理画面へのリンクを表示するだけなので、モデルとしての機能はほとんどないです。

<?php
/* メニュー モデル */

// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
require(__DIR__.'/db.php');

class menu extends db{
  public function __construct(){
    //親コンストラクタを呼ぶ
    parent::__construct();
    unset($_SESSION["confirm_data"]);
  }
}

_model/change_password.php

dbクラスを継承したパスワード変更 モデルです。

<?php
/* パスワード変更 モデル */

// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
require(__DIR__.'/db.php');

class change_password extends db{
  public $err=[
    "user_password1"=>"",
    "user_password2"=>"",
  ];

  public function __construct(){
    //親コンストラクタを呼ぶ
    parent::__construct();
    //POST値の取得
    $this->post["user_password1"]=$this->post["user_password1"] ?? "";
    $this->post["user_password2"]=$this->post["user_password2"] ?? "";
  }

  //POSTの取得と値のチェック
  //戻り値 true:エラー有り false:エラーなし
  public function getCheckPost(){
    [$flag, $this->err["user_password1"]] = checkpass($this->post["user_password1"]);
    [$temp, $this->err["user_password2"]] = check2pass($this->post["user_password1"],$this->post["user_password2"]);
    if($temp){$flag=true;}
    
    //現在のパスワードハッシュを取得する
    $sql="SELECT user_login_hash FROM tm_user WHERE user_id=?";
    $ret=$this->execSQL($sql,[$this->user_id]);
    if(empty($ret)){return true;}
    //現在のパスワードと新しいパスワードが同じ場合はNGとする
    $verify=password_verify($this->post["user_password1"],$ret[0]["user_login_hash"]);
    if($verify){
      $flag=true;
      $this->err["user_password1"]="現在と同じパスワードは設定できません";
    }
    
    if($flag){
      unset($_SESSION["confirm_data"]);
    }else{
      // トークンが未発行なら新規発行(確認画面用キー)
      if(empty($this->post["confirm_token"])){
        $token = bin2hex(random_bytes(16));
        $this->post["confirm_token"] = $token;
        $this->posten["confirm_token"] = $token;
      } else {
        $token = $this->post["confirm_token"];
      }
      $_SESSION["confirm_data"][$token]["hash"] = password_hash($this->post["user_password1"], PASSWORD_DEFAULT);
    }
    return $flag;
  }

  public function passwordUpdate(){
    // フォームから送信されたワンタイムトークンを取得
    $token = $this->post["confirm_token"] ?? "";
    // セッションの該当トークン内にハッシュが存在するか確認
    $password_hash = $_SESSION["confirm_data"][$token]["hash"] ?? null;
    $result=false;
    if(!empty($password_hash)){
      //パスワード更新処理
      $sql="UPDATE tm_user SET user_login_hash=?, user_upid=?, user_login_hash_date=NOW() WHERE user_id=?";
      $this->execSQL(
        $sql,
        [
          $password_hash,
          $this->user_id, // user_upid 用
          $this->user_id  // WHERE user_id 用
        ]
      );
      session_regenerate_id(true);
      $result=true;
    }
    unset($_SESSION["confirm_data"]);
    return $result;
  }
}

_model/manage_user.php

dbクラスを継承したユーザー追加変更 モデルです。

<?php
/* 利用者登録・更新 モデル */

// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
require(__DIR__.'/db.php');

class manage_user extends db{
  public $users;//利用者一覧配列
  public $permissions;//権限一覧配列(コンストラクタで取得)
  public $disableds;//使用不可一覧配列(コンストラクタで取得)
  //エラーメッセージが入る
  public $err=[
    "user_name"=>"",
    "user_login_id"=>"",
    "user_password1"=>"",
    "user_password2"=>"",
    "user_permission"=>"",
    "user_disabled"=>""
  ];


  //コンストラクタ
  public function __construct(){
    //親コンストラクタを呼ぶ
    parent::__construct();
    //権限一覧の取得
    $this->getPermissions();
    //使用可否一覧の取得
    $this->getDisableds();
    //POST値の取得

    $this->post["edit_user_id"]        =(int)($this->post["edit_user_id"] ?? 0);
    $this->post["user_name"]      =$this->post["user_name"] ?? "";
    $this->post["user_login_id"]  =$this->post["user_login_id"] ?? "";
    $this->post["user_password1"] =$this->post["user_password1"] ?? "";
    $this->post["user_password2"] =$this->post["user_password2"] ?? "";
    $this->post["user_permission"]=(int)($this->post["user_permission"] ?? 0);
    $this->post["user_disabled"]  =(int)($this->post["user_disabled"] ?? 1);

    //不正な値チェック
    if(!preg_match("/^\d{1,6}$/",$this->post["edit_user_id"])){
      //数値以外が含まれていたら(新規扱い)
      $this->post["edit_user_id"]=0;
    }
    $this->posten = $this->escapeRecursive($this->post);
  }


  //ユーザー一覧取得
  public function getUsers(){
    unset($_SESSION["confirm_data"]);
    $sql="
      SELECT
        user_id, user_name, user_login_id, user_login_hash_date,
        user_login_date, user_fail_count,
        user_permission, perm_permission_mean,
        user_disabled, disa_disabled_mean
      FROM tm_user
      INNER JOIN tm_permission
        ON user_permission=perm_permission
      INNER JOIN tm_disabled
        ON user_disabled=disa_disabled
      ORDER BY user_id
    ";
    $ret = $this->execSQL($sql,[]);
    if(empty($ret)){
      $this->users=[];
    }else{
      $this->users = $this->escapeRecursive($ret);//エンティティする
    }
  }


  //指定ユーザーの情報を取得する
  public function getUser($target_user_id){
    $target_user_id=(int) $target_user_id;
    unset($_SESSION["confirm_data"]);
    $sql="
      SELECT user_id, user_name, user_login_id, user_permission, user_disabled
      FROM tm_user
      WHERE user_id=?
    ";
    $ret=$this->execSQL($sql,[$target_user_id]);
    if(empty($ret)){
      $this->post["edit_user_id"]=0;
      $this->post["user_name"]="新規ユーザー";
      $this->post["user_login_id"]="";
      $this->post["user_password1"]="";
      $this->post["user_password2"]="";
      $this->post["user_permission"]=0;
      $this->post["user_disabled"]=0;
      //エンティティ
      $this->posten=$this->escapeRecursive($this->post);
      return false;
    }else{
      $this->post["edit_user_id"]=$target_user_id;
      $this->post["user_name"]=$ret[0]["user_name"];
      $this->post["user_login_id"]=$ret[0]["user_login_id"];
      $this->post["user_permission"]=$ret[0]["user_permission"];
      $this->post["user_disabled"]=$ret[0]["user_disabled"];
      $this->post["user_password1"]="";
      $this->post["user_password2"]="";
      //エンティティ
      $this->posten=$this->escapeRecursive($this->post);
      return true;
    }
  }


  //新規ユーザー登録時のPOSTの取得と値のチェック($isRegistがtrueの時は登録時でパスワードチェックだけ行わない)
  //戻り値 true:エラー有り false:エラーなし
  public function getCheckPost($isRegist=false){
    $flag=false;
    //対象ユーザーIDの不正な値チェック
    if(!preg_match("/^\d{1,6}$/",$this->post["edit_user_id"])){
      //数値以外が含まれていたらNG
      $flag=true;
    }else{
      //対象ユーザーIDの不正な値チェック
      if($this->post["edit_user_id"]!=0){
        $sql="SELECT count(user_id) ct FROM tm_user WHERE user_id=?";
        $ret=$this->execSQL($sql,[$this->post["edit_user_id"]]);
        if(empty($ret)){
          $flag=true;
        }
      }
    }
    //ユーザー名の確認
    if(mb_strlen($this->post["user_name"])<1||mb_strlen($this->post["user_name"])>16){
      $flag=true;
      $this->err["user_name"]="1~16文字で入力してください";
    }
    //新規登録時
    if($this->post["edit_user_id"]==0){
      //ログインIDのチェック
      if(!preg_match("/^[0-9a-zA-Z@\-\_\.]{4,50}$/",$this->post["user_login_id"])){
        $flag=true;
        $this->err["user_login_id"]="半角英数4~50文字で入力してください";
      }
      //ログインIDの重複チェック
      $sql="SELECT user_id FROM tm_user where user_login_id=?";
      $ret=$this->execSQL($sql,[$this->post["user_login_id"]]);
      if(!empty($ret)){
        //既に存在するログインID
        $flag=true;
        $this->err["user_login_id"]="既に存在するIDのため登録できません";
      }
    }
    
    //新規 又は 既存でパスワード変更時はパスワードチェック
    if( 
      !$isRegist &&
      (
        $this->post["edit_user_id"]==0 || 
        ($this->post["edit_user_id"]!=0 && (!empty($this->post["user_password1"]) || !empty($this->post["user_password2"]))) 
      )
    ){
      [$temp,$this->err["user_password1"]] = checkpass($this->post["user_password1"]);
      if($temp){$flag=true;}
      [$temp, $this->err["user_password2"]] = check2pass($this->post["user_password1"], $this->post["user_password2"]);
      if($temp){$flag=true;}
    }
    //権限チェック
    if (isset($this->permissions[$this->post["user_permission"]]) && ($this->post["user_permission"]>0) ) {
      $this->posten["perm_permission_mean"] = $this->permissions[$this->post["user_permission"]];
    } else {
      $flag = true;
      $this->err["user_permission"] = "無効な権限が選択されています";
    }

    //使用可否チェック
    if (isset($this->disableds[$this->post["user_disabled"]])) {
      $this->posten["disa_disabled_mean"] = $this->disableds[$this->post["user_disabled"]];
    } else {
      $flag = true;
      $this->err["user_disabled"] = "無効な値が選択されています";
    }

    //変更登録時に管理者権限0人チェック
    if($this->post["edit_user_id"]!=0 && ($this->post["user_disabled"]==1 || $this->post["user_permission"]!=1)){
      $sql="SELECT COUNT(user_id) ct FROM tm_user where user_id<>? and user_permission=1 and user_disabled=0";
      $ret=$this->execSQL($sql,[$this->post["edit_user_id"]]);
      if(!empty($ret) && (int)$ret[0]["ct"] === 0){
        $flag=true;
        if($this->post["user_permission"]!=1){
          $this->err["user_permission"] = "管理者が居なくなるので変更不可";
        }
        if($this->post["user_disabled"]==1){
          $this->err["user_disabled"] = "管理者が居なくなるので変更不可";
        }
      }
    }
    //入力値に不正がない場合はハッシュ値を保存
    if($flag){
      unset($_SESSION["confirm_data"]);
    }else{
      // トークンが未発行なら新規発行(確認画面用キー)
      if(empty($this->post["confirm_token"])){
        $token = bin2hex(random_bytes(16));
        $this->post["confirm_token"] = $token;
        $this->posten["confirm_token"] = $token;
      } else {
        $token = $this->post["confirm_token"];
      }
      if(!empty($this->post["user_password1"])){
        $_SESSION["confirm_data"][$token]["hash"] = password_hash($this->post["user_password1"], PASSWORD_DEFAULT);
      }
    }
    return $flag;
  }


  //権限一覧の取得
  private function getPermissions(){
    $sql="
      SELECT perm_permission, perm_permission_mean
      FROM tm_permission
      ORDER BY perm_permission
    ";
    $ret=$this->execSQL($sql,[]);
    $ret2=[];
    if(is_array($ret)) {
      foreach ($ret as $row) {
        $ret2[$row["perm_permission"]] = $row["perm_permission_mean"];
      }
    }
    $this->permissions = $this->escapeRecursive($ret2);
  }


  //使用可否一覧の取得
  private function getDisableds(){
    $sql="
      SELECT disa_disabled,disa_disabled_mean
      FROM tm_disabled
      ORDER BY disa_disabled
    ";
    $ret=$this->execSQL($sql,[]);
    $ret2=[];
    if(is_array($ret)) {
      foreach ($ret as $row) {
        $ret2[$row["disa_disabled"]] = $row["disa_disabled_mean"];
      }
    }
    $this->disableds = $this->escapeRecursive($ret2);
  }


  //利用者の追加、更新処理
  public function userInsertUpdate(){
    // フォームから送信されたワンタイムトークンを取得
    $token = $this->post["confirm_token"] ?? "";
    // セッションの該当トークン内にハッシュが存在するか確認
    $password_hash = $_SESSION["confirm_data"][$token]["hash"] ?? null;
    try {
      $this->pdo->beginTransaction();
      if($this->post["edit_user_id"] != 0 && ($this->post["user_disabled"] == 1 || $this->post["user_permission"] != 1)){
        $sql = "SELECT COUNT(user_id) ct FROM tm_user WHERE user_id <> ? AND user_permission = 1 AND user_disabled = 0 FOR UPDATE";
        $ret = $this->execSQL($sql, [$this->post["edit_user_id"]]);
        if(!empty($ret) && (int)$ret[0]["ct"] === 0){
          throw new Exception("他の管理者によって変更が行われたため、この管理者を無効化・変更できません。");
        }
      }
      if($this->post["edit_user_id"]==0){
        if($password_hash===null){die();}
        //挿入処理
        $sql="
          INSERT INTO tm_user(
            user_name, user_login_id, user_login_hash, user_permission, user_disabled, user_upid, user_regid
          ) VALUES(
            ?, ?, ?, ?, ?, ? ,?
          )";
        $this->execSQL(
          $sql,
          [
            $this->post["user_name"],
            $this->post["user_login_id"],
            $password_hash,
            $this->post["user_permission"],
            $this->post["user_disabled"],
            $this->user_id,
            $this->user_id,
          ]
        );
      }else{
        //更新処理
        if(empty($password_hash)){
          //パスワードを含まない更新処理
          $sql="UPDATE tm_user SET user_name=?,user_permission=?,user_disabled=?, user_upid=? WHERE user_id=?";
          $this->execSQL(
            $sql,
            [
              $this->post["user_name"],
              $this->post["user_permission"],
              $this->post["user_disabled"],
              $this->user_id,
              $this->post["edit_user_id"],
            ]
          );
        }else{
          //パスワードを含む更新処理
          $sql="UPDATE tm_user SET user_name=?, user_login_hash=?, user_permission=?, user_disabled=?, user_upid=?, user_login_hash_date=NOW() WHERE user_id=?";
          $this->execSQL(
            $sql,
            [
              $this->post["user_name"],
              $password_hash,
              $this->post["user_permission"],
              $this->post["user_disabled"],
              $this->user_id,
              $this->post["edit_user_id"],
            ]
          );
        }
      }
      //コミット
      $this->pdo->commit();
      session_regenerate_id(true);
    }catch(Exception $e){
      // エラー時はロールバック
      if ($this->pdo->inTransaction()) {
          $this->pdo->rollBack();
      }
      throw $e;
    }
    unset($_SESSION["confirm_data"]);
  }

}

コントローラ

コントローラは利用者が接続してくるURLです。
コントローラでは入力値のバリデーションチェックを行うのですが、本記事ではモデルが行っています。
なので処理がほとんどありません。

login/index.php

ログイン画面を表示するコントローラ

<?php
/* ログイン コントローラ */

require_once __DIR__.'/../_inc/config.php';

//ビューのインクルード
require_once __DIR__.'/../_view/login/index.php';

login/login_check.php

ログインチェックを行うコントローラ

<?php
/* ログインチェック コントローラー */

//モデルのインクルード
require_once __DIR__.'/../_model/login.php';
$model=new login();


//入力値チェック
if(empty($model->post["user_login_id"])||empty($model->post["user_password"])){
  header("location: ./");
  exit;
}

if($model->canLogin()){
  unset($_SESSION["login_err"]);
  unset($_SESSION["user_login_id"]);
  header("location: ../menu/");
  exit;
}else{
  $_SESSION["login_err"]=$model->err["login_err"];
  $_SESSION["user_login_id"]=$model->posten["user_login_id"];
  header("location: ./");
  exit;
}

logout/index.php

ログアウト コントローラ

<?php
/* ログアウトコントローラー */

require_once __DIR__.'/../_model/logout.php';
$model=new logout();

$model->logout();

//ビューのインクルード
require_once __DIR__.'/../_view/logout/index.php';

menu/index.php

メインメニューを表示するコントローラ

<?php
/* メニュー コントローラ */

//モデルのインクルード
require_once __DIR__.'/../_model/menu.php';

$model=new menu();

//Sessionの有効性確認:Sessionが無効ならログアウトに遷移
if($model->isValidSession()==false){
  header("location: ../logout/");
  exit;
}

//パスワードの変更が必要な場合にパスワード変更画面へ遷移
if($model->user_need_change_password){
  header("location: ../change_password/");
  exit;
}

//ビューのインクルード
require_once __DIR__.'/../_view/menu/index.php';

change_password/index.php

パスワード変更入力 コントローラー

<?php
/* パスワード変更入力コントローラー */

//モデルのインクルード
require_once __DIR__.'/../_model/change_password.php';
$model=new change_password();

//Sessionの有効性確認:Sessionが無効ならログアウトに遷移
if($model->isValidSession()==false){
  header("location: ../logout/");
  exit;
}

//ビューのインクルード
require_once __DIR__.'/../_view/change_password/confirm.php';

change_password/index.php

パスワード変更確認 コントローラー

<?php
/* パスワード変更確認コントローラー */

//モデルのインクルード
require_once __DIR__.'/../_model/change_password.php';
$model=new change_password();

//Sessionの有効性確認:Sessionが無効ならログアウトに遷移
if($model->isValidSession()==false){
  header("location: ../logout/");
  exit;
}

$ret=$model->getCheckPost();
if($ret){
  //エラー時のビューのインクルード
  require_once __DIR__.'/../_view/change_password/index.php';
}else{
  //CSRF用乱数生成(セッション変数 $_SESSION["csrf"] にも入る。
  //ビューで$model->csrfをhiddenに入れる必要がある
  $model->createCSRF();
  //確認画面ビューのインクルード
  require_once __DIR__.'/../_view/change_password/confirm.php';
}

change_password/regist.php

パスワード変更登録 コントローラー

<?php
/* パスワード変更登録コントローラー */

//モデルのインクルード
require_once __DIR__.'/../_model/change_password.php';
$model=new change_password();

//Sessionの有効性確認:Sessionが無効ならログアウトに遷移
if($model->isValidSession()==false){
  header("location: ../logout/");
  exit;
}

if(!$model->equalCSRF()){
  //CSRF対策
  header("location: ../logout/");
  exit;
}

if($model->passwordUpdate()){
  //登録完了画面へ遷移
  header("location: ./complete.php");
  exit;
}else{
  //エラー時
  header("location: ../logout/");
  exit;
}

change_password/regist.php

パスワード変更完了 コントローラー

<?php
/* パスワード変更完了コントローラー */

//モデルのインクルード
require_once __DIR__.'/../_model/change_password.php';
$model=new change_password();

//Sessionの有効性確認:Sessionが無効ならログアウトに遷移
if($model->isValidSession()==false){
  header("location: ../logout/");
  exit;
}

unset($_SESSION["user_password1"]);
unset($_SESSION["user_password2"]);
unset($_SESSION["user_password1_en"]);
unset($_SESSION["user_password2_en"]);

//完了画面ビューのインクルード
require_once __DIR__.'/../_view/change_password/complete.php';

manage_user/index.php

ユーザー管理(ユーザー一覧) コントローラー

<?php
/* ユーザー管理(ユーザー一覧)コントローラー */

//モデルのインクルード
require_once __DIR__.'/../_model/manage_user.php';
$model=new manage_user();

//Sessionの有効性確認:Sessionが無効ならログアウトに遷移
if($model->isValidSession()==false){
  header("location: ../logout/");
  exit;
}

//管理者権限が無いのでログアウトに遷移
if($model->user_permission!=1){
  header("location: ../logout/");
  exit;
}

$model->getUsers();

//ビューのインクルード
require_once __DIR__.'/../_view/manage_user/index.php';

manage_user/input.php

ユーザー登録入力 コントローラー

<?php
/* ユーザー登録入力コントローラー */

//モデルのインクルード
require_once __DIR__.'/../_model/manage_user.php';
$model=new manage_user();

//SESSIONの有効性確認:SESSIONが無効ならログアウトに遷移
if($model->isValidSession()==false){
  header("location: ../logout/");
  exit;
}

//管理者権限が無いのでログアウトに遷移
if($model->user_permission!=1){
  header("location: ../logout/");
  exit;
}

if(isset($_POST["user_disabled"])){
  //確認画面から戻ってきた場合
  //入力チェック、バリデーションチェック
  $model->getCheckPost();
}else{
  $model->getUser($model->post["edit_user_id"]);
}
//ビューのインクルード
require_once __DIR__.'/../_view/manage_user/input.php';

manage_user/confirm.php

ユーザー登録確認 コントローラー

<?php
/* ユーザー登録確認コントローラー */

//モデルのインクルード
require_once __DIR__.'/../_model/manage_user.php';
$model=new manage_user();

//Sessionの有効性確認:Sessionが無効ならログアウトに遷移
if($model->isValidSession()==false){
  header("location: ../logout/");
  exit;
}

//管理者権限が無いのでログアウトに遷移
if($model->user_permission!=1){
  header("location: ../logout/");
  exit;
}

//入力チェック、バリデーションチェック
$ret=$model->getCheckPost();

if($ret){
  //エラー時のビューのインクルード
  require_once __DIR__.'/../_view/manage_user/input.php';
}else{
  //CSRF用乱数生成(セッション変数 $_SESSION["csrf"] にも入る)
  //ビューで$model->csrfをhiddenに入れる必要がある
  $model->createCSRF();
  //確認画面ビューのインクルード
  require_once __DIR__.'/../_view/manage_user/confirm.php';
}

manage_user/regist.php

ユーザー登録 コントローラー

<?php
/* ユーザー登録コントローラー */

//モデルのインクルード
require_once __DIR__.'/../_model/manage_user.php';
$model=new manage_user();

//Sessionの有効性確認:Sessionが無効ならログアウトに遷移
if($model->isValidSession()==false){
  header("location: ../logout/");
  exit;
}

//管理者権限が無いのでログアウトに遷移
if($model->user_permission!=1){
  header("location: ../logout/");
  exit;
}

if(!$model->equalCSRF()){
  die("CSRF エラー");
  //CSRF対策、合致しない場合は不正アクセス
  header("location: ../logout/");
  exit;
}

//入力チェック、バリデーションチェック
$ret=$model->getCheckPost(true);

if($ret){
  //エラー時
  die("バリデーション エラー");
  header("location: ../logout/");
  exit;
}else{
  //登録処理を行う
  $model->userInsertUpdate();
  //登録完了画面へ遷移
  header("location: ./complete.php");
  exit;
}

manage_user/complete.php

ユーザー登録完了 コントローラー

<?php
/* ユーザー登録完了コントローラー */

//モデルのインクルード
require_once __DIR__.'/../_model/manage_user.php';
$model=new manage_user();

//Sessionの有効性確認:Sessinが無効ならログアウトに遷移
if($model->isValidSession()==false){
  header("location: ../logout/");
  exit;
}

//管理者権限が無いのでログアウトに遷移
if($model->user_permission!=1){
  header("location: ../logout/");
  exit;
}

unset($_SESSION["user_password1"]);
unset($_SESSION["user_password2"]);
unset($_SESSION["user_password1_en"]);
unset($_SESSION["user_password2_en"]);


//完了画面ビューのインクルード
require_once __DIR__.'/../_view/manage_user/complete.php';

ビュー

ビューは表示を行います。

_view/login/index.php

ログイン画面を表示するビュー

<?php
// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
?>
<html lang="ja">
<head>
<?php require __DIR__."/../../_inc/head.php"; ?>
<script>
window.addEventListener("DOMContentLoaded",function(){
  document.querySelector("#login").addEventListener("click",function(){
    if(document.querySelector("#user_login_id").value!=="" && document.querySelector("#user_password").value!==""){
      let form=document.querySelector("#login_form");
      form.action="./login_check.php";
      form.method="post";
      form.submit();
    }
  });
});
</script>
</head>
<body>
  <h1><?=TITLE?></h1>
  <form id="login_form">
    <div class="rounded-input" style="margin-top:40px;">
      <div>
        <div>ログインID</div>
        <input type="text" id="user_login_id" name="user_login_id" placeholder="IDを入力" autocomplete="off" maxlength="50" value="<?= $_SESSION["user_login_id"] ?? "" ?>">
      </div>
      <div>
        <div>パスワード</div>
        <input type="password" id="user_password" name="user_password" placeholder="パスワードを入力" autocomplete="new-password" maxlength="20">
      </div>
    </div>
  </form>
  <div style="text-align:center;color:red;" id="login_err"><?= $_SESSION["login_err"] ?? "" ?></div>
  <div style="text-align:center;">
    <input type="button" value="ログイン"  id="login" tabindex="0">
  </div>
</body>
</html>

_view/logout/index.php

ログアウト画面を表示するビュー

<?php
// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
?>
<html lang="ja">
<head>
<?php require_once __DIR__."/../../_inc/head.php"; ?>
  <meta http-equiv="refresh" content="1.2; url=../login/">
</head>
<body>
  <h1><?=TITLE?></h1>
  <p>ログアウト</p>
</body>
</html>

_view/menu/index.php

メニュー画面を表示するビュー

<?php
// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <?php require(__DIR__.'/../../_inc/head.php'); ?>
</head>
<body>
  <h1 class="standard"><?=TITLE?>[<?=$model->user_name?>]</h1>
  <h2 class="standard">メニュー</h2>

  <nav class="menu">
    <ul>
      <?php if($model->user_permission==1){ ?>
        <li><a href="../manage_user/">ユーザー管理</a></li>
      <?php } ?>
      <li><a href="../change_password/">パスワード変更</a></li>
      <li><a href="../logout/">ログアウト</a></li>
    </ul>
  </nav>
</body>
</html>

_view/change_password/index.php

パスワード変更入力画面を表示するビュー

<?php
// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <?php require(__DIR__.'/../../_inc/head.php'); ?>
  <script>
    window.addEventListener("load",function(){
      document.querySelector("#submitBtn").addEventListener("click",function(){
          document.querySelector("#form1").submit();
      });
    });
    function ch_pass(param){
      let eo=document.querySelector(param+"+span .eyeopen");
      let ec=document.querySelector(param+"+span .eyeclose");
      let elm=document.querySelector(param);
      if(elm.type=="text"){
        elm.type="password";
        eo.style.display="inline";
        ec.style.display="none";
      }else{
        elm.type="text";
        eo.style.display="none";
        ec.style.display="inline";
      }
    }
  </script>

</head>
<body>
  <h1 class="standard"><?=TITLE?>[<?=$model->user_name?>]</h1>
  <h2 class="standard">パスワード変更(入力)</h2>


  <div style="display:flex;width:100%;">
    <?php if($model->user_need_change_password){ ?>
      <p style="color:red;">パスワード変更が必要です</p>
    <?php }else{ ?>
      <input type="button" value="戻る" style="margin-left:auto;" onclick="location.href='../menu/'">
    <?php } ?>
  </div>

  <form method="POST" action="./confirm.php" id="form1">
    <div class="manage_user_input">
      <div>
        <div class="pass">パスワード<br><span>英数と記号を含めて8~20文字<br>/.@!?#$%&=-_|+*(){}~<>;\:[]</span></div>
        <div class="input_password">
          <input type="password" id="user_password1" name="user_password1" maxlength="20" autocomplete="new-password" inputmode="text" />
          <span onclick="ch_pass('#user_password1')">
            <svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" style="width:1em;height:auto;" class="eyeopen">
              <path d="M4,16 A29,14 0,0,1 60,16" style="fill:none;stroke:#000;stroke-width:4;" />
              <circle style="fill:#EEE;stroke:#000;stroke-width:4;" cx="32" cy="36" r="18" />
              <circle style="fill:#000;stroke:none;stroke-width:0;" cx="32" cy="36" r="10" />
              <circle style="fill:#FFF;stroke:none;stroke-width:0;" cx="38" cy="32" r="4" />
            </svg>
            <svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" style="width:1em;height:auto;display:none;" class="eyeclose">
              <path d="M4,16 A29,14 0,0,1 60,16" style="fill:none;stroke:#888;stroke-width:4;" />
              <circle style="fill:#EEE;stroke:#888;stroke-width:4;" cx="32" cy="36" r="18" />
              <circle style="fill:#888;stroke:none;stroke-width:0;" cx="32" cy="36" r="10" />
              <circle style="fill:#FFF;stroke:none;stroke-width:0;" cx="38" cy="32" r="4" />
              <line x1="2" y1="62" x2="62" y2="2" style="stroke:#000;stroke-width:2;" />
            </svg>
          </span>
        </div>
        <div class="alert"><?=$model->err["user_password1"]?></div>
      </div>
      <div>
        <div class="pass">パスワード(確認)<br><span>上記と同じもの</span></div>
        <div class="input_password">
          <input type="password" id="user_password2" name="user_password2" maxlength="20" autocomplete="new-password" inputmode="text" />
          <span onclick="ch_pass('#user_password2')" class="input_password">
            <svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" style="width:1em;height:auto;" class="eyeopen">
              <path d="M4,16 A29,14 0,0,1 60,16" style="fill:none;stroke:#000;stroke-width:4;" />
              <circle style="fill:#EEE;stroke:#000;stroke-width:4;" cx="32" cy="36" r="18" />
              <circle style="fill:#000;stroke:none;stroke-width:0;" cx="32" cy="36" r="10" />
              <circle style="fill:#FFF;stroke:none;stroke-width:0;" cx="38" cy="32" r="4" />
            </svg>
            <svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" style="width:1em;height:auto;display:none;" class="eyeclose">
              <path d="M4,16 A29,14 0,0,1 60,16" style="fill:none;stroke:#888;stroke-width:4;" />
              <circle style="fill:#EEE;stroke:#888;stroke-width:4;" cx="32" cy="36" r="18" />
              <circle style="fill:#888;stroke:none;stroke-width:0;" cx="32" cy="36" r="10" />
              <circle style="fill:#FFF;stroke:none;stroke-width:0;" cx="38" cy="32" r="4" />
              <line x1="2" y1="62" x2="62" y2="2" style="stroke:#000;stroke-width:2;" />
            </svg>
          </span>
        </div>
        <div class="alert"><?=$model->err["user_password2"]?></div>
      </div>
    </div>
    <div style="margin:0;width:100%;text-align:center;">
      <input type="button" value="確認" id="submitBtn">
    </div>
  </form>

  <div style="display:flex;width:100%;">
      <input type="button" value="戻る" style="margin-left:auto;" onclick="location.href='../menu/'">
  </div>

</body>
</html>

_view/change_password/confirm.php

パスワード変更確認画面を表示するビュー

<?php
// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <?php require(__DIR__.'/../../_inc/head.php'); ?>
  <script>
    window.addEventListener("load",function(){
      document.querySelector("#submitBtn").addEventListener("click",submitBtnClick);
    });
    function submitBtnClick(){
      //連打防止
      document.querySelector("#submitBtn").removeEventListener("click",submitBtnClick);
      let fm=document.querySelector("#form1");
      fm.action="./regist.php";
      fm.submit();
    }
  </script>
</head>
<body>
  <h1 class="standard"><?=TITLE?>[<?=$model->user_name?>]</h1>
  <h2 class="standard">パスワード変更(確認)</h2>

  <div style="display:flex;">
    <input type="button" value="戻る" style="margin-left:auto;" onclick="location.href='./'">
  </div>
  <form method="POST" id="form1">
    <input type="hidden" id="csrf" name="csrf" value="<?=$model->csrf?>">
    <div class="manage_user_input">
      <div>
        <div>
          パスワードを変更しても宜しいですか?
        </div>
      </div>
    </div>
    <div style="margin;0;width;100%;text-align:center;">
      <input type="hidden" name="confirm_token" value="<?=$model->posten["confirm_token"]?>">
      <input type="button" id="submitBtn" value="登録">
    </div>
  </form>
  <div style="display:flex;">
    <input type="button" value="戻る" style="margin-left:auto;" onclick="location.href='./'">
  </div>
</body>
</html>

_view/change_password/complete.php

パスワード変更完了画面を表示するビュー

<?php
// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <?php require(__DIR__.'/../../_inc/head.php'); ?>
</head>
<body>
  <h1 class="standard"><?=TITLE?>[<?=$model->user_name?>]</h1>
  <h2 class="standard">パスワード変更(完了)</h2>
  <div class="manage_user_complete">
    パスワード変更が完了しました<br><span style="font-size:0.7em;font-weight:bold;">変更したパスワードを忘れないでください</span>
  </div>
  <div style="margin;0;width;100%;text-align:center;">
      <input type="button" value="メニューへ戻る" style="margin-left:auto;" onclick="location.href='../menu/'">
  </div>
</body>
</html>

_view/manage_user/index.php

ユーザー管理(ユーザー一覧)画面を表示するビュー

<?php
// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <?php require(__DIR__.'/../../_inc/head.php'); ?>
</head>
<body>
  <h1><?=TITLE?>[<?=$model->user_name?>]</h1>
  <h2>利用者登録</h2>

  <div style="display:flex;">
    <form method="post" action="./input.php">
      <input type="hidden" name="edit_user_id" value="0">
      <input type="button" value="新規登録" onclick="this.parentElement.submit()">
    </form>
    <input type="button" value="戻る" style="margin-left:auto;" onclick="location.href='../menu/'">
  </div>
  <table class="tbl">
    <thead>
      <tr>
        <th style="width:128px;">変更</th>
        <th>管理者番号</th>
        <th>名前</th>
        <th>ログインID</th>
        <th>パスワード変更日時<br>ログイン日時</th>
        <th>ログイン<br>エラー回数</th>
        <th>権限</th>
        <th>使用可否</th>
      </tr>
    </thead>
    <tbody>
      <?php
        foreach($model->users as $user){
      ?>
        <tr>
          <td>
            <form method="post" action="./input.php">
              <input type="hidden" name="edit_user_id" id="edit_user_id" value="<?=$user["user_id"]?>">
              <input type="button" value="変更" onclick="this.parentElement.submit()">
            </form>
          </td>
          <td><?=$user["user_id"]?></td>
          <td><?=$user["user_name"]?></td>
          <td><?=$user["user_login_id"]?></td>
          <td><?=$user["user_login_hash_date"]?><br><?=$user["user_login_date"]?></td>
          <td><?=$user["user_fail_count"]?></td>
          <td><?=$user["perm_permission_mean"]?></td>
          <td><?=$user["disa_disabled_mean"]?></td>
        </tr>
      <?php
        }
      ?>
    </tbody>
  </table>

  <div style="display:flex;">
    <div style="margin-left:auto;">
      <input type="button" value="戻る" style="margin-left:auto;" onclick="location.href='../menu/'">
    </div>
  </div>

</body>
</html>

_view/manage_user/input.php

ユーザー登録入力画面を表示する ビュー。

<?php
// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <?php require(__DIR__.'/../../_inc/head.php'); ?>
  <script>
    window.addEventListener("load",function(){
      <?php /* 登録時チェック */ ?>
      document.querySelector("#submitBtn").addEventListener("click",function(){
          document.querySelector("#form1").submit();
      });
    });
  </script>
  <script>
    function ch_pass(param){
      let eo=document.querySelector(param+"+span .eyeopen");
      let ec=document.querySelector(param+"+span .eyeclose");
      let elm=document.querySelector(param);
      if(elm.type=="text"){
        elm.type="password";
        eo.style.display="inline";
        ec.style.display="none";
      }else{
        elm.type="text";
        eo.style.display="none";
        ec.style.display="inline";
      }
    }
  </script>
</head>
<body>
  <h1 class="standard"><?=TITLE?>[<?=$model->user_name?>]</h1>
  <h2 class="standard">利用者登録<?=$model->posten["edit_user_id"]==0?"(新規入力)":"(変更入力)"?></h2>
  <div style="display:flex;">
    <input type="button" value="戻る" style="margin-left:auto;" onclick="location.href='./'">
  </div>
  <form method="POST" action="./confirm.php" id="form1" autocomplete="off">
    <input type="hidden" id="edit_user_id" name="edit_user_id" value="<?=$model->posten["edit_user_id"]?>">
    <div class="manage_user_input">
      <div>
        <div>名前<br><span>1~16文字</span></div>
        <div>
          <input type="text" id="user_name" name="user_name" value="<?=$model->posten["user_name"]?>"  maxlength="16" autocomplete="off"/>
          <div class="alert"><?=$model->err["user_name"]?></div>
        </div>
      </div>
      <?php
        if($model->posten["edit_user_id"]==0){
      ?>
        <div>
          <div>ログインID(重複不可)<br><span>半角英数4~50文字<br>半角記号@-_.は使用可能</span></div>
          <div>
            <input type="text" id="user_login_id" name="user_login_id" value="<?=$model->posten["user_login_id"]?>" maxlength="50" autocomplete="off" inputmode="url"/>
            <div class="alert"><?=$model->err["user_login_id"]?></div>
          </div>
        </div>
      <?php
        }else{
      ?>
        <div>
          <div>ログインID(重複不可)</div>
          <div>
            <?=$model->posten["user_login_id"]?>
          </div>
        </div>
      <?php
        }
        ?>
      <div>
        <div>パスワード<br><span>英数字記号含む8~20文字<br>/.@!?#$%&=-_|+*(){}~<>;\:[]<?=$model->posten["edit_user_id"]!=0?"<br>変更時のみ入力":""?></span></div>
        <div class="input_password">
          <input type="password" id="user_password1" name="user_password1" value="" maxlength="20" autocomplete="new-password" />
          <span onclick="ch_pass('#user_password1')" class="input_password">
            <svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" style="width:1em;height:auto;" class="eyeopen">
              <path d="M4,16 A29,14 0,0,1 60,16" style="fill:none;stroke:#000;stroke-width:4;" />
              <circle style="fill:#EEE;stroke:#000;stroke-width:4;" cx="32" cy="36" r="18" />
              <circle style="fill:#000;stroke:none;stroke-width:0;" cx="32" cy="36" r="10" />
              <circle style="fill:#FFF;stroke:none;stroke-width:0;" cx="38" cy="32" r="4" />
            </svg>
            <svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" style="width:1em;height:auto;display:none;" class="eyeclose">
              <path d="M4,16 A29,14 0,0,1 60,16" style="fill:none;stroke:#888;stroke-width:4;" />
              <circle style="fill:#EEE;stroke:#888;stroke-width:4;" cx="32" cy="36" r="18" />
              <circle style="fill:#888;stroke:none;stroke-width:0;" cx="32" cy="36" r="10" />
              <circle style="fill:#FFF;stroke:none;stroke-width:0;" cx="38" cy="32" r="4" />
              <line x1="2" y1="62" x2="62" y2="2" style="stroke:#000;stroke-width:2;" />
            </svg>
          </span>
          <div class="alert"><?=$model->err["user_password1"]?></div>
        </div>
      </div>
      <div>
        <div>パスワード(確認)<br><span>上記と同じもの<?=$model->posten["edit_user_id"]!=0?"<br>変更時のみ入力":""?></span></div>
        <div class="input_password">
          <input type="password" id="user_password2" name="user_password2" value="" maxlength="20" autocomplete="new-password" />
          <span onclick="ch_pass('#user_password2')" class="input_password">
            <svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" style="width:1em;height:auto;" class="eyeopen">
              <path d="M4,16 A29,14 0,0,1 60,16" style="fill:none;stroke:#000;stroke-width:4;" />
              <circle style="fill:#EEE;stroke:#000;stroke-width:4;" cx="32" cy="36" r="18" />
              <circle style="fill:#000;stroke:none;stroke-width:0;" cx="32" cy="36" r="10" />
              <circle style="fill:#FFF;stroke:none;stroke-width:0;" cx="38" cy="32" r="4" />
            </svg>
            <svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" style="width:1em;height:auto;display:none;" class="eyeclose">
              <path d="M4,16 A29,14 0,0,1 60,16" style="fill:none;stroke:#888;stroke-width:4;" />
              <circle style="fill:#EEE;stroke:#888;stroke-width:4;" cx="32" cy="36" r="18" />
              <circle style="fill:#888;stroke:none;stroke-width:0;" cx="32" cy="36" r="10" />
              <circle style="fill:#FFF;stroke:none;stroke-width:0;" cx="38" cy="32" r="4" />
              <line x1="2" y1="62" x2="62" y2="2" style="stroke:#000;stroke-width:2;" />
            </svg>
          </span>
          <div class="alert"><?=$model->err["user_password2"]?></div>
        </div>
      </div>
      <div>
        <div>権限</div>
        <div>
          <select class="mamselect" id="user_permission" name="user_permission">
            <?php
              foreach($model->permissions as $key=>$value){
            ?>
              <option value="<?=$key?>"
                <?=$key==$model->posten["user_permission"]?"selected":""?>
              >
                <?=$value?>
              </option>
            <?php
              }
            ?>
          </select>
          <div class="alert"><?=$model->err["user_permission"]?></div>
        </div>
      </div>
      <div>
        <div>使用可否</div>
        <div>
          <select class="mamselect" id="user_disabled" name="user_disabled">
            <?php
              foreach($model->disableds as $key=>$value){
            ?>
              <option value="<?=$key?>"
                <?=$key==$model->posten["user_disabled"]?"selected":""?>
              >
                <?=$value?>
              </option>
            <?php
              }
            ?>
          </select>
          <div class="alert"><?=$model->err["user_disabled"]?></div>
        </div>
      </div>
    </div>
    <div style="margin:0;width:100%;text-align:center;">
      <input type="button"id="submitBtn" value="確認">
    </div>
  </form>
  <div style="display:flex;">
      <input type="button" value="戻る" style="margin-left:auto;" onclick="location.href='./'">
  </div>


</body>
</html>

_view/manage_user/confirm.php

ユーザー登録確認画面を表示する ビュー。

<?php
// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <?php require(__DIR__.'/../../_inc/head.php'); ?>
  <script>
    window.addEventListener("load",function(){
      document.querySelector("#back").addEventListener("click",function(){
        let fm=document.querySelector("#form1");
        fm.action="./input.php";
        fm.submit();
      });
      document.querySelector("#back2").addEventListener("click",function(){
        let fm=document.querySelector("#form1");
        fm.action="./input.php";
        fm.submit();
      });
      document.querySelector("#submitBtn").addEventListener("click",submitBtnClick);
    });
    function submitBtnClick(){
      //連打防止
      document.querySelector("#submitBtn").removeEventListener("click",submitBtnClick);
      let fm=document.querySelector("#form1");
      fm.action="./regist.php";
      fm.submit();
    }
  </script>
  <link rel="stylesheet" href="../css/manage_user.css">
</head>
<body>
  <?php /* include(__DIR__.'/../../_inc/burger_menu.php'); */ ?>
  <h1 class="standard"><?=TITLE?>[<?=$model->user_name?>]</h1>
  <h2 class="standard">利用者登録<?=$model->post["edit_user_id"]==0?"(新規確認)":"(変更確認)"?></h2>
  <div style="display:flex;">
    <input type="button" id="back" value="戻る" style="margin-left:auto;">
  </div>
  <form method="POST" id="form1">
    <input type="hidden" id="csrf" name="csrf" value="<?=$model->csrf?>">
    <input type="hidden" id="edit_user_id" name="edit_user_id" value="<?=$model->posten["edit_user_id"]?>">
    <input type="hidden" id="user_name" name="user_name" value="<?=$model->posten['user_name']?>">
    <input type="hidden" id="user_login_id" name="user_login_id" value="<?=$model->posten['user_login_id']?>">
    <input type="hidden" id="user_permission" name="user_permission" value="<?=$model->posten['user_permission']?>">
    <input type="hidden" id="user_disabled" name="user_disabled" value="<?=$model->posten['user_disabled']?>">
    <div class="manage_user_input">
      <div>
        <div>名前</div>
        <div class="pad-l">
          <?=$model->posten["user_name"]?>
        </div>
      </div>
      <?php
        if($model->posten["edit_user_id"]==0){
      ?>
        <div>
          <div>ログインID</div>
          <div class="pad-l">
            <?=$model->posten["user_login_id"]?>
          </div>
        </div>
      <?php
        }
      ?>
      <?php
        if(!empty($_SESSION["user_password_hash"])){
      ?>
        <div>
          <div>パスワード</div>
          <div class="pad-l">
            ****************
          </div>
        </div>
      <?php
        }
      ?>
        <div>
          <div>権限</div>
          <div class="pad-l">
            <?=$model->posten["perm_permission_mean"]?>
          </div>
        </div>
      <div>
        <div>使用可否</div>
        <div class="pad-l">
          <?=$model->posten["disa_disabled_mean"]?>
        </div>
      </div>
    </div>
    <div style="margin;0;width;100%;text-align:center;">
      <input type="hidden" name="confirm_token" value="<?=$model->posten["confirm_token"]?>">
      <input type="button" id="submitBtn" value="登 録" style="margin-left:auto;">
    </div>
  </form>
  <div style="display:flex;">
    <input type="button" id="back2" value="戻る" style="margin-left:auto;">
  </div>
</body>
</html>

_view/manage_user/complete.php

ユーザー登録完了画面を表示する ビュー。

<?php
// 直接アクセスされた場合は404やアクセス拒否を返す
if(  realpath(__FILE__)===realpath($_SERVER['SCRIPT_FILENAME'])  ){
    header('HTTP/1.1 403 Forbidden');
    die('403 Forbidden');
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <?php require(__DIR__.'/../../_inc/head.php'); ?>
</head>
<body>
  <h1 class="standard"><?=TITLE?>[<?=$model->user_name?>]</h1>
  <h2 class="standard">利用者登録(完了)</h2>
  <div class="manage_user_complete">
    利用者処理が完了しました
  </div>
  <div style="margin;0;width;100%;text-align:center;">
    <input type="button" value="利用者登録へ戻る" onclick="location.href='./'">
  </div>
</body>
</html>

CSS

css/common.css

CSSファイル

@charset "utf-8";

:root{
  --font-xs:16px;
  --font-s:18px;
  --font-m:20px;
  --font-l:24px;
  --font-xl:30px;
  
  --font-col-base:#333399;
  --font-col-light:#BBCCFF;
  --font-col-dark:#000066;

  --font-red:#FF0000;
  --font-green:#009900;
  --font-blue:#0000FF;

  --bg-base:#F4F8FF;
  --bg-light:#DDEEFF;
  --bg-dark:#3333AA;
  
  --line-base:#333399;
  --line-light:#6666CC;
  --line-dark:#000066;
}

html, body{
  margin: 0;
  padding: 0;
  font-family: "JetBrains Mono","IBM Plex Sans JP","游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN",
               "Hiragino Sans", "Meiryo UI", "メイリオ", Meiryo,
               "MS Pゴシック", "Helvetica Neue", Arial, sans-serif;
  color: var(--font-col-base);
  font-size: var(--font-s);
  word-break: break-all;
  word-wrap: break-word;
  box-sizing: border-box;
  -webkit-text-size-adjust: 100%;
  background: var(--bg-base);
}


h1{font-size:var(--font-xl);}
h2{font-size:var(--font-l);}
h3{font-size:var(--font-m);}
h4{font-size:var(--font-s);}
h5{font-size:var(--font-xs);}
h1{
  color:var(--font-col-dark);
  border-left: none;
  border-bottom: 0.1em solid var(--line-base);
  margin:0 0 0.5em 0;
}
h2{
  color:var(--font-col-dark);
  border-left: 0.5em solid var(--line-light);
  margin:0 0 0.5em 0;
  padding-left:0.2em;
}


/* 角丸入力 */
div.rounded-input{
  box-sizing:border-box;
  font-size:var(--font-n);
  width:100%;
  margin:0;
  text-align:center;
}
  div.rounded-input>div{
    box-sizing:border-box;
    border:1px solid var(--line-base);
    margin:1em auto;
    padding:1em 0.8em 0.5em 0.5em;
    border-radius:0.25em;
    position:relative;
    min-width:160px;
    max-width:400px;
    width:100%;
    flex:1 0 200px;
    font-size:1em;
    background:transparent;
  }
    /* 見出し文字 */
    div.rounded-input>div>div{
      box-sizing:border-box;
      position:absolute;
      border:none;
      background:var(--bg-base);
      font-size:1em;
      top:-0.7em;
      left:1em;
      padding:0;
      margin:0;
    }

    div.rounded-input>div>input::placeholder {
      color: var(--font-col-light);
      opacity: 1;
    }

    div.rounded-input>div>input[type="text"],
    div.rounded-input>div>input[type="password"]{
      box-sizing:border-box;
      border:none;
      outline:none;
      width:100%;
      font-size:var(--font-s);
      height:calc(var(--font-s) * 2);
      background:transparent;
    }


    div.rounded-input>div>select{
      box-sizing:border-box;
      border:none;
      outline:none;
      width:100%;
      font-size:var(--font-s);
    }
    div.rounded-input>div>input[type="radio"]{
      box-sizing:border-box;
      width:0.8em;
      height:0.8em;
      font-size:var(--font-s);
    }
    div.rounded-input>div>input[type="checkbox"]{
      width:0.8em;
      height:0.8em;
      font-size:var(--font-s);
    }

input[type="button"]{
  padding:0.2em 0.4em;
  font-size: var(--font-m);
  color: var(--font-col-light);
  background-color: var(--bg-dark);
  border: 2px solid var(--line-dark);
  border-radius: 0.6em;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
}

input[type="button"]:hover {
  background-color: var(--bg-light);
  color: var(--font-col-dark);
}

input[type="button"]:active {
  background-color: var(--bg-dark);
  transform: translateY(1px); /* 押した感 */
}

input , select{
  font-size: var(--font-s);
  margin: 0.2em;
}
input[type="text"] , input[type="password"]{
  width:240px;
  max-width:calc(100vw - 40px);
  min-width:calc(240px);
}
select{
  width:240px;
  max-width:calc(100vw - 40px);
  min-width:calc(240px);
}


.menu>ul {
  list-style: none;
  padding: 0;
  margin: 20px 0;
}
.menu>ul>li {
  margin-bottom: 0.8em;
}
.menu>ul>li>a {
  display: block;
  padding: 0.6em 1em;
  font-size: var(--font-m);
  margin:0.2em;
  color: var(--font-col-light);
  background: var(--bg-dark);
  border: 1px solid var(--line-light);
  border-radius: 0.5em;
  text-decoration: none;
  transition: 0.1s;
}
.menu>ul>li>a:hover {
  background: var(--bg-base);
  border-color: var(--line-light);
}
.menu>ul>li>a:active {
  background: var(--bg-light);
  border-color: var(--line-dark);
  transform: translateY(1px);
}

div.pass{
  font-size:var(--font-m);
  font-weight:bold;
}
div.pass>span{
  font-size:var(--font-xs);
  font-weight:normal;
}



div.manage_user_input{
  box-sizing:border-box;
  display:flex;
  flex-wrap:wrap;
  border:none;
  margin:0;
  padding:0.5em;
  overflow-wrap:anywhere;
  word-break:break-all;
  width:100%;
}
  div.manage_user_input>div{
    display:flex;
    flex-wrap:wrap;
    box-sizing:border-box;
    margin:0 0 20px 0;
    padding:0;
    width:100%;
  }
    div.manage_user_input>div>div{
      width:100%;
    }

.input_password{
  position:relative;
}
.input_password>span{
  cursor:pointer;
  font-size:var(--font-l);
  position:absolute;
  top:0;
  transform:translate( -120%, 25% );
}

div.alert{
  color:var(--font-red);
}


.tbl{border-collapse: collapse;}
.tbl tr>th{background:var(--bg-light);}
.tbl tr>td, .tbl tr>th{
  border:1px solid var(--line-base);
  font-size:var(--font-xs);
}

.manage_user_input{
  display:flex;
  flex-wrap:wrap;

}
.manage_user_input>div{
  display:flex;
  flex-wrap:wrap;
  font-size:var(--font-m);
}

.manage_user_input>div>div:nth-of-type(1){
  font-size:var(--font-m);
  font-weight:bold;
  width:30%;
  max-width:300px;
  min-width:260px;
}
.manage_user_input>div>div:nth-of-type(1)>span{
  font-size:var(--font-xs);
  font-weight:normal;
}

.manage_user_input>div>div:nth-of-type(2){
  font-size:var(--font-s);
  width:70%;
  max-width:260px;
  min-width:160px;
}
.manage_user_input>div>div:nth-of-type(2)>div{
  font-size:var(--font-xs);
}

リダイレクトファイル

index.php

ログインにリダイレクトするファイル。

<?php
header("location: ./login/");

PHPサンプル集一覧へ