😆

Next.jsでパスワードを生成してみる

に公開

Next.jsでパスワードジェネレータを作ったのでそれについて書いていこうと思います!

生成機能を作ってみる

元データを用意する

パスワードにはアルファベットの大文字・小文字、数字、記号32文字を使えるようにしたいので元となるデータを用意します!

// アルファベット大文字
const aplbig = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
// アルファベット小文字
const aplsml = 'abcdefghijklmnopqrstuvwxyz';
// 数字
const num = '0123456789';
// 記号
const sym = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~';

const syms1 = [
 { name: 'backQuote', value: '`' }, { name: 'waterLine', value: '~' }, { name: 'exclamation', value: '!' }, { name: 'atMark', value: '@' }, { name: 'sharp', value: '#' }, { name: 'dollar', value: '$' }, { name: 'percent', value: '%' }, { name: 'accentCircon', value: '^' },
];
const syms2 = [
 { name: 'and', value: '&' }, { name: 'asterisk', value: '*' }, { name: 'openParentheses', value: '(' }, { name: 'closeParentheses', value: ')' }, { name: 'underBar', value: '_' }, { name: 'plus', value: '+' }, { name: 'hyphen', value: '-' }, { name: 'equal', value: '=' },
];
const syms3 = [
 { name: 'openCurly', value: '{' }, { name: 'closeCurly', value: '}' }, { name: 'openBracket', value: '[' }, { name: 'closeBracket', value: ']' }, { name: 'backSlash', value: '\\' }, { name: 'pipeline', value: '|' }, { name: 'colon', value: ':' }, { name: 'semicolon', value: ';' },
];
const syms4 = [
 { name: 'doubleQuotation', value: '"' }, { name: 'singleQuotation', value: "'" }, { name: 'lessThan', value: '<' }, { name: 'greaterThan', value: '>' }, { name: 'comma', value: ',' }, { name: 'period', value: '.' }, { name: 'question', value: '?' }, { name: 'slash', value: '/' }
];
const symlist = syms1.concat(syms2, syms3, syms4);

メイン部分を作る

まず、作ったパスワードを入れる箱を作ります

+ "use client"

+ import { useState } from 'react';
+ export default function Main() {
+   const [data, setData] = useState([{ id: 1, pass: '' }]);
+ }

次に、パスワードを生成する部分を作ります

export default function Main() {
+   function generetePassfrase() {
+       let passbase = '';
+       passbase += aplsml;
+       passbase += aplbig;
+       passbase += num;
+       const symbols = symlist
+         .map((sym) => sym.value)
+         .join('');
+       passbase += symbols;
+       let savepass = [];
+       // 配列にパスワードを保存
+       for (let i = 1; i <= 10; i++) {
+           let pass = '';
+           // パスワードを作成
+           for (let j = 0; j < 12; j++) {
+               pass += passbase[Math.floor(Math.random() * passbase.length)];
+           }
+           savepass.push({ id: i, pass: pass });
+       }
+       setData(savepass);
+       passbase = '';
+   }
+}
}
  1. まずlet passbase = '';で空のパスワードベースを作ります
  2. 次に加算代入でベースに追加していきます
  3. let savepass = [];で仮の生成したパスワードを集める場所(配列)を作り、
  4. for (let i = 1; i <= 10; i++) {}で10個のパスワードを生成します(i <= 10
  5. 次にまず仮のパスワードを保存する場所を作ります(let pass = ''
  6. for (let j = 0; j < 12; j++) {}で12桁のパスワードを生成します(j < 12
  7. pass += passbase[Math.floor(Math.random() * passbase.length)];でランダムにパスワードを生成しpassに加算代入で指定した文字数分代入します
  8. savepass.push({ id: i, pass: pass });savepassに指定した個数分要素を追加します
  9. setData(savepass)で先ほど用意した保存場所(data)にsavepassを代入し保存します
  10. 最後にpassbase = '';でパスワードベースを無に帰します

フロント部分を作る

+ import { Button, Paper, TableContainer, Table, TableHead, TableRow, TableCell, TableBody } from '@mui/material';
+ import { Grid } from '@mui/material/Unstable_Grid2';

export default function Main() {
+  return (
+  <Grid container spacing={2}>
+      <Grid xs={12}>
+        <Button
+          fullWidth
+          variant="contained"
+          color="primary"
+          onClick={() => {
+            generetePassfrase();
+          }}
+        >
+          パスワードを生成する
+        </Button>
+      </Grid>
+       <Grid xs={12}>
+         <TableContainer component={Paper}>
+           <Table sx={{ minWidth: '100%', maxWidth: '100%' }}aria-label="Password">
+             <TableHead>
+               <TableRow>
+                 <TableCell>No.</TableCell>
+                 <TableCell align="right">Password</TableCell>
+               </TableRow>
+             </TableHead>
+             <TableBody>
+               {data.map((row) => (
+                 <TableRow
+                   key={row.id}
+                   sx={{
+                     '& td': { wordBreak: 'break-all' },
+                     '&:last-child td, &:last-child th': { border: 0 },
+                   }}
+                 >
+                   <TableCell component="th" scope="row">
+                     {row.id}
+                   </TableCell>
+                   <TableCell align="right">
+                     {row.pass}
+                   </TableCell>
+                 </TableRow>
+               ))}
+             </TableBody>
+           </Table>
+         </TableContainer>
+       </Grid>
+    </Grid>
+  )
}

data.mapで保存したパスワード分行を生成しています。
言わずもがなですが<Button>onClick={() => { generatePassfrase() }}でパスワードを生成する関数を実行しています。

パスワードの種類等を変更する機能を作る

先述の手段でパスワードを生成するコードが完成しました!
しかし、これでは強制的にアルファベット大文字・小文字、数字、記号を使用した12桁のパスワードが生成されてしまい、利便性が最悪なので次にパスワードの種類・文字数を選べる機能を作っていこうと思います!

事前準備

export default function Main() {
+  const [length, setLength] = useState('12');
+  const [passwordType, setPasswordType] = useState({
+    alpsml: true, alpbig: true, num: true, sym: false
+  });
+  const [symType, setSymType] = useState({
+    backQuote: true, waterLine: true, exclamation: true, atMark: true, sharp: true, dollar: true, percent: true, accentCircon: true, and: true, asterisk: true, openParentheses: true, closeParentheses: true, underBar: true, plus: true, hyphen: true, equal: true, openCurly: true, closeCurly: true, openBracket: true, closeBracket: true, backSlash: true, pipeline: true, colon: true, semicolon: true, doubleQuotation: true, singleQuotation: true, lessThan: true, greaterThan: true, comma: true, period: true, question: true, slash: true
+  });
}

まず最初に、const [length, setLength] = useState('12');でパスワードの文字数を格納する箱を作ります。(デフォルトは12)
次にconst [passwordType, setPasswordType] = useState({ ~~~ });でアルファベット大文字・小文字、数字、記号を使用するかどうかを格納する箱を作ります。
const [symType, setSymType] = useState({ ~~~ });では記号をそれぞれ使うかどうか選択できるように箱を作っています。

オプション画面の作成

- import { Button, Paper, TableContainer, Table, TableHead, TableRow, TableCell, TableBody } from '@mui/material';
+ import { Button, Paper, TableContainer, Table, TableHead, TableRow, TableCell, TableBody, Accordion, AccordionSummary, AccordionDetails } from '@mui/material';
+ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';

export default function Main() {
  return (
    <Grid container spacing={2}>
+      <Grid xs={12}>
+        <Accordion>
+          <AccordionSummary
+            expandIcon={<ExpandMoreIcon />}
+            aria-controls="panel1-content"
+            id="panel1-header"
+          >
+            オプション
+          </AccordionSummary>
+          <AccordionDetails>
+
+          </AccordionDetails>
+        </Accordion>
+      </Grid>
    </Grid>
  )
}

まず、<Accordion>で骨組みを作ります

- import { Button, Paper, TableContainer, Table, TableHead, TableRow, TableCell, TableBody, Accordion, AccordionSummary, AccordionDetails } from '@mui/material';
+ import { Button, Paper, TableContainer, Table, TableHead, TableRow, TableCell, TableBody, Accordion, AccordionSummary, AccordionDetails, MenuItem, FormLabel, FormControl, Select } from '@mui/material';

export default function Main() {
+  function handlelengthChange(event) {
+   setLength(event.target.value);
+  }
  return (
      <Grid xs={12}>
        <Accordion>
        <AccordionDetails>
+         <Grid container spacing={2}>
+           <Grid xs={12} md={6}>
+             <FormControl fullWidth>
+             <FormLabel id="length">パスワードの長さ</FormLabel>
+               <Select
+                 labelId="length"
+                 id="length"
+                 defaultValue={'12'}
+                 onChange={handlelengthChange}
+                 fullWidth
+               >
+                 {Array.from({ length: 255 }, (_, i) => i + 1).map((value) => (
+                   <MenuItem key={value} value={value.toString()}>
+                     {value}
+                   </MenuItem>
+                 ))}
+               </Select>
+             </FormControl>
+           </Grid>
+         </Grid>
        </AccordionDetails>
        </Accordion>
      </Grid>
  )
}

次に、パスワードの長さを設定するフォームを作ります。
{Array.from({ length: 255 }, (_, i) => i + 1).map((value) => ( ~~~ ))}では{length: 255}の数だけ選択できる数字のボタンを生成します。
数字のボタンを押下(<Select>が変動)するとonChange={}によってhandlelengthChangeが動き、あらかじめ用意してあるパスワードの桁数を保存する箱(length)に選択した数字を格納します。

- import { Button, Paper, TableContainer, Table, TableHead, TableRow, TableCell, TableBody, Accordion, AccordionSummary, AccordionDetails, MenuItem, FormLabel, FormControl, Select } from '@mui/material';
+ import { Button, Paper, TableContainer, Table, TableHead, TableRow, TableCell, TableBody, Accordion, AccordionSummary, AccordionDetails, MenuItem, FormLabel, FormControl, FormGroup, Select, Checkbox } from '@mui/material';
export default function Main() {
  return (
    <Grid xs={12}>
      <Accordion>
        <AccordionDetails>
          <Grid container spacing={2}>
+         <Grid xs={12} md={6}>
+           <FormControl fullWidth required error={typeError}>
+             <FormLabel component="legend">パスワードの種類</FormLabel>
+               <FormGroup>
+                 <FormControlLabel
+                   control={
+                     <Checkbox
+                       onChange={(event) =>
+                           setPasswordType({ ...passwordType, alpsml: event.target.checked })
+                       }
+                       checked={passwordType.alpsml}
+                       name="alpsml"
+                     />
+                   }
+                 label="小文字"
+               />
+               <FormControlLabel
+                 control={
+                   <Checkbox
+                     onChange={(event) =>
+                       setPasswordType({ ...passwordType, alpbig: event.target.checked })
+                     }
+                     checked={passwordType.alpbig}
+                     name="alpbig"
+                   />
+                 }
+               label="大文字"
+             />
+             <FormControlLabel
+               control={
+                 <Checkbox
+                   onChange={(event) =>
+                     setPasswordType({ ...passwordType, num: event.target.checked })
+                   }
+                   checked={passwordType.num}
+                   name="num"
+                 />
+               }
+             label="数字"
+           />
+           </FormGroup>
+         </Grid>
          </Grid>
        </AccordionDetails>
      </Accordion>
    </Grid>
  )
}

ここでは作りたい種類のチェックボックスをいじくると先ほど作った各文字種を使うかどうかを格納する箱(passwordType)にtrueまたはfalseを入れてくれる機能を作っています。
onChange={(event) => setPasswordType({ ~~~ })}passwordType内の該当する要素を更新しています。
また、初期でtrueとなっているものもあるのでchecked={ ~~~ }を使用して初期からチェックを入れてくれるようにもしてあります。

export default function Main() {
  return (
    <Grid xs={12}>
      <Accordion>
        <AccordionDetails>
          <Grid container spacing={2}>
            <Grid xs={12} md={6}>
            <FormGroup>
+             <FormControlLabel
+               control={
+                 <Checkbox
+                   checked={passwordType.sym}
+                   onChange={(event) =>
+                     setPasswordType({ ...passwordType, sym: event.target.checked })
+                   }
+                   name="sym"
+                 />
+               }
+             label="記号"
+             />
              </FormGroup>
            </Grid>
+           {passwordType.sym && (
+             <Grid xs={6} md={3}>
+               <FormControl fullWidth>
+                 <FormLabel id="length">使用する記号の種類</FormLabel>
+                   <FormGroup>
+                     {syms1.map((sym) => (
+                       <FormControlLabel
+                         key={sym.name}
+                         control={
+                           <Checkbox
+                               onChange={(event) =>
+                                 setSymType({ ...symType, [sym.name]: event.target.checked })
+                               }
+                             checked={symType[sym.name]}
+                             name={sym.name}
+                           />
+                         }
+                         label={sym.value}
+                       />
+                     ))}
+                   </FormGroup>
+                 </FormControl>
+               </Grid>
+             )}
+             {passwordType.sym && (
+               <Grid xs={6} md={3}>
+                 <FormControl fullWidth>
+                   <FormLabel></FormLabel>
+                   <FormGroup>
+                     {syms2.map((sym) => (
+                       <FormControlLabel
+                         key={sym.name}
+                         control={
+                           <Checkbox
+                             onChange={(event) =>
+                               setSymType({ ...symType, [sym.name]: event.target.checked })
+                             }
+                             checked={symType[sym.name]}
+                             name={sym.name}
+                           />
+                         }
+                         label={sym.value}
+                       />
+                     ))}
+                   </FormGroup>
+                 </FormControl>
+               </Grid>
+             )}
+             {passwordType.sym && (
+               <Grid xs={6} md={3}>
+                 <FormControl fullWidth>
+                   <FormLabel></FormLabel>
+                   <FormGroup>
+                     {syms3.map((sym) => (
+                       <FormControlLabel
+                         key={sym.name}
+                         control={
+                           <Checkbox
+                             onChange={(event) =>
+                               setSymType({ ...symType, [sym.name]: event.target.checked })
+                             }
+                             checked={symType[sym.name]}
+                             name={sym.name}
+                           />
+                         }
+                         label={sym.value}
+                       />
+                     ))}
+                   </FormGroup>
+                 </FormControl>
+               </Grid>
+             )}
+             {passwordType.sym && (
+               <Grid xs={6} md={3}>
+                 <FormControl fullWidth>
+                   <FormLabel></FormLabel>
+                   <FormGroup>
+                     {syms4.map((sym) => (
+                       <FormControlLabel
+                         key={sym.name}
+                         control={
+                           <Checkbox
+                             onChange={(event) =>
+                               setSymType({ ...symType, [sym.name]: event.target.checked })
+                             }
+                             checked={symType[sym.name]}
+                             name={sym.name}
+                           />
+                         }
+                       label={sym.value}
+                     />
+                   ))}
+                 </FormGroup>
+               </FormControl>
+             </Grid>
+           )}
          </Grid>
        </AccordionDetails>
      </Accordion>
    </Grid>
  )
}

ここでは記号を使うかどうか、使う場合は使う記号の種類を選択できる機能を作っています。
記号を使う、つまりpasswordType.symがtrueの場合は「使用する記号の種類」というフォームが表示されます。
true,falseの代入などは先述の通りです。

エラーハンドリングなどなど

- import { useState } from 'react';
+ import { useState, useEffect } from 'react';

export default function Main() {
+  const [typeError, setTypeError] = useState(false);
+  useEffect(() => {
+    if (!Object.values(passwordType).includes(true)) {
+      setTypeError(true);
+    } else {
+      setTypeError(false);
+    }
+  }, [passwordType]);
  return (
    <Grid container spacing={2}>
      <Grid xs={12}>
        <Accordion>
          <AccordionDetails>
            <Grid container spacing={2}>
              <Grid xs={12} md={6}>
+               {typeError && (
+                 <FormHelperText error>パスワードの種類を選択してください</FormHelperText>
+               )}
              </Grid>
            </Grid>
          </AccordionDetails>
        </Accordion>
      </Grid>
    </Grid>
  )
}

ここでは「パスワードの種類」が何も選択されていない場合にtypeErrorをtrueとし、エラーメッセージを表示する機能を作成しています。
useEffect(() => { ~~~ }, [passwordType]);でuseEffectを使用することによってpasswordTypeが変化(チェックリストを押下)する度にエラーに該当するかどうかをチェックしています。

export default function Main() {
+  useEffect(() => {
+    if (!Object.values(symType).includes(true)) {
+      setPasswordType({ ...passwordType, sym: false });
+      setSymType({ ...symType, backQuote: true });
+    }
+  }, [symType]);
}

ここではsymTypeにtrueが存在しない=使用する記号の種類が選択されていない場合に使用する文字種から記号を取り除いています。
ここでsetSymType({ ...symType, backQuote: true });を使用しているのはpasswordType.symがfalseになった場合でも最低1つは記号の種類をtrueにしておくことによりシステムに矛盾が生じないようにするためです。

+ import ContentCopyIcon from '@mui/icons-material/ContentCopy';

export default function Main() {
+  function clipCopy(copyText) {
+    navigator.clipboard.writeText(copyText).then(
+      function () {
+        alert("コピーしました!")
+      },
+      function () {
+        alert("コピーに失敗しました!")
+      }
+    );
+  }
   return (
     <Grid container spacing={2}>
       <Grid xs={12}>
         <TableContainer component={Paper}>
           <Table>
             <TableBody>
               {data.map((row) => (
                 <TableRow>
                    <TableCell align="right">{row.pass}
+                     <Button
+                       onClick={() => {
+                         clipCopy(row.pass);
+                       }}
+                       disabled={row.pass === ''}
+                     >
+                       <ContentCopyIcon />
+                     </Button>
                    </TableCell>
                  </TableRow>
                ))}
              </TableBody>
            </Table>
          </TableContainer>
        </Grid>
      </Grid>
   )
}

エラーとは関係ないのですが、利便性向上のためボタン1つでクリップボードへコピーできる機能をここでは作っています。

仕上げ

export default function Main() {
  function generetePassfrase() {
+   if (Object.values(passwordType).includes(true)) {
      let passbase = '';
+     if (passwordType.alpsml) {
        passbase += aplsml;
+     }
+     if (passwordType.alpbig) {
        passbase += aplbig;
+     }
+     if (passwordType.num) {
        passbase += num;
+     }
+     if (passwordType.sym) {
+       const trueSymTypes = Object.entries(symType)
+         .filter(([key, value]) => value === true)
+         .map(([key, value]) => key);
       const symbols = symlist
+         .filter((sym) => trueSymTypes.includes(sym.name))
          .map((sym) => sym.value)
          .join('');
        passbase += symbols;
+     }
+   } else {
+     setTypeError(true);
+   }
  }
}

最後に仕上げとして肝心のパスワードを生成する部分に使用する文字種としてチェックが入っているのかどうか、記号を使用する場合はどの記号を使用するのかを判断する機能を作成しています。
これでパスワードの種類などを変更する機能は完成です!!!

最後に

ここまでの長文をお読みいただきありがとうございました!
コピペ用のGistを用意したのでぜひご利用くださいませ!
https://gist.github.com/hnt1028/6db022898d6c844023170144186243b0

Discussion