video-distribution-service (学習メモ)

actionsディレクトリのなかのserver側のtsファイルは、デフォルトで'use server'が備わっているわけではないので、ファイルの頭に'use server'をつけることを忘れてはいけない

現在supabaseでのauth処理を
を参考に実装中
NextRequestについて後で記事をまとめる

NextUiのButtonでsupabaseのloginとsign up のロジックを組んでいたが、Buttonコンポーネントのformactionは機能しなかった。だから、htmlのbuttonを使用した

drizzle ORMを使って、supabaseのdatabaseを操作する

drizzle orm を使うときは、
npm i react
npm i drizzle-orm
npm i -D drizzle-kit
をして
npm i dotenv

drizzle ORMとsupabaseのdatabaseをつなげるときのstring variable
Database Settingsに移動し、Connection StringセクションからURIをコピーする。接続プーリングを使用していることを確認してください。パスワード・プレースホルダを実際のデータベース・パスワードに置き換えることを忘れないでください。

postgresを使うために
npm install postgres
を実行

スキーマ作った
import {pgTable, serial, text} from "drizzle-orm/pg-core";
export const user=pgTable('user',{
id:serial('id').primaryKey(),
userName:text('user_name').notNull(),
email:text('email').notNull(),
password:text('password').notNull()
})

drizzle.configを設定

schemaをpushできなくて足踏みしている
drizzleを使ってsupabaseのdatabaseにアクセスしたい

これのintegrationsでできるかも

DATABASE_URLの[YOUR-PASSWORD]のところを変えていなかった

drizzle-kit push
したらsupabaseにdatabase Tableができた

databaseが機能しているのか調べたい
loginのロジックを作成
これ使えばauthフォーム簡単に作れるかも
使うのをやめた

どうやら、auth認証はできていたみたい
ただ、supabaseのauth management にuserが追加されないので、これを解決しに行く

signoutを作成した
この後、auth managementを解決する

const supabase=createClient()
const {data}=await supabase.auth.getUser();
でuserのデータがあることがわかった
最終的には、userごとにページを出したい
そのために
・drizzleでsupabaseにデータを渡す処理を作成する

のconnection with drizzleの部分を参考にしてexport const dbを作成
const connection=process.env.DATABASE_URL as string
環境変数は後ろから型定義

db.tsに
import as * schema from "./schema"
const db=drizzle(client,{schema});
を追加

const supabase=createClient();
const {data,error}=await supabase.auth.getUser();
if(error||!data.user){
redirect('/login)
}
これをサーバー側なら、createClientをserverからとってこれば、ログインした場合にしか表示されないページになる

signupの方にdatabaseへのinsertをしなければいけないことに今気づいた
また、databaseへのinsertのときに、
no overloads matches call...
のエラーが出て格闘していたが、valuesのところにカーソルをあてて詳細をみたら、userIdとemailの型が、string || undefinedになっていた。これにより、undefinedの可能性があり、insertできなかった。
よって下記のようにundefinedの可能性を消した
if(error||!data.user?.id||!data.user?.email||!data.user)
emailに関しては、
await db.insert(user).values({
userId:data.user.id,
email:formUser.email,
password:formUser.password
})
とすれば、if文からemailの箇所を消去しても良い

この後は、databaseに入っている、つまりsignupされたユーザーはdatabaseにつかされない処理を書く
そのために、queryでdatabaseからuserテーブルのdataをとってくる

actionsのsignupのdatabase.insertの不具合を修正している

supabaseのdataにuserの情報が入っていない

signInのときの
const {data,error}=await supabase.auth.signInWithPassword(formUserData)
のdataにはuser情報が入っていた

https://orm.drizzle.team/docs/migrations
これの、
export default defineConfig({
out:'./supabase/migrations',
})
追加して、
npx drizzle-kit generate
したらdataが表示された

おそらく、manageのUsersに追加する方法はあると思う。
これらの記事を頼りに考えてみる
queryファイルの
import {cache} from "react";
export const getAccounts=cache(async()=>{
})
とcacheを付けた

drizzleだけではauthはできなそう。SQLを書くしかない
いろいろ試したがdata:userが邪魔をする

npm install @supabase/auth-helpers-nextjs
実行した

.envファイルのNEXT_PUBLIC_SUPABASE_URLとNEXT_PUBLIC_SUPABASE_ANON_KEYの値がsupabaseの別のプロジェクトのを使っていた。
直したら、Userに入った。時間を使った結果この初歩的なミスだったのがつらいけど、うれしさの方が大きい。
今後、ちゃんと.envファイルの環境変数の値が正しいのかを確認する
自分がやっていたことは、間違っていなかったことがなにより嬉しい

errorページの表示に苦戦していたが、supabaseのmiddlewareの下の方を訂正した。
...
const {
data: { user },
} = await supabase.auth.getUser()
if (!user&&!request.nextUrl.pathname.startsWith('/error') && !request.nextUrl.pathname.startsWith('/login')&&!request.nextUrl.pathname.startsWith('/auth')) {
// no user, potentially respond by redirecting the user to the login page
const url = request.nextUrl.clone()
url.pathname = '/login'
return NextResponse.redirect(url)
}
return supabaseResponse

schemaに、videos, pack, noteのスキーマを作成した

query.tsにgetVideoを作成した
useactionsというserver actionファイルを作成した
そこに、postVideoという関数を作成した

videoをどうやって、databaseに入れるか

file storageに入れるのか?

supabas storageを使えばよい

supabase storageにvideo bucketを作成した。
そして、<innput type="file"/>でアップロードできるようにしている

これを参考にinput type ="file"を導入する

uploadErrorが実行される

(やめた)
storageのnew policy、allow ~ 、select,insert,update,deleteにチェック、policy definitionをtrueに設定する

const fileExt=file.name.split('.').pop()
const filePath=`${userId}-${Math.random()}.${fileExt}`
この行が重要
この動画の、policy設定が重要
とりあえずuploadはできる

downloadを作成する

uploadするとき、fileのpathをdatabaseにおくる。
すべての動画を表示する時:
自分の動画だけ表示:userIdを含むもの.schemaの構造を利用。

serve actionにuseStateの値を渡すことは、できない

計画
- アップロード順になるように並び替える
- データベースに代入

アップロード順になるようにロジックを組む
const videos=await getVideo();
if(video){
video.sort((a,b)=>{
//a, b は配列の中の要素を見る
cosnst dateA=new Date(a.updated_at);
const dateB=new Date(b.updated_at);
return dateB.getTime()-date.A.getTime()
//baのときは大きい順、abのときは小さい順
})
}

databaseにvideoのtitle,description,videoSrcを入れる。
const video={
videoTitle:videoInformation.videoTitle,
description:videoInformation.description,
videoSrc:filePath,
user_id:userId
}
postVideo(video)
postVideoは以下
export const postVideo=async(video:video)=>{
await db.insert(videos).values({
videoTitle: video.videoTitle,
videoSrc:video.videoSrc,
description: video.description,
user_id: video.user_id
})
revalidatePth('/','layout')
}
queryにgetUserVideoを作成
export const getUserVideo=cache(async(userId:string)=>{
const data=await db.query.videos.findMany({
where:eq(videos.user_id,useId)
})
return data;
})

新たな問題
storageのvideoを消しても、databaseの値が消えないのでエラーが出る
storageとdatabaseをつなげる

Error: Text content does not match server-rendered HTML.
See more info here: https://nextjs.org/docs/messages/react-hydration-error
の発生
hydration-errorとは
hydration:htmlにjavascriptを埋め込むこと。webページの生成は、ssrの場合、サーバーから静的なhtmlを送り、後でjavascriptを追加する。このことをhydration(水分補給)。からっからのhtmlにjavascriptを入れて水分補給するイメージ。

{userVideo.reverse.map((video) => {
return (
<div key={video.id}>
<Card video={video}/>
</div>
)
})}
userVideoの後ろのreverseを消したら解決した。おそらく、reverseはjavascriptなので、一回画面を表示した後に、reverseが行われるせいで、投げたhtmlと表示したhtmlに差異が生まれていた

'use client'つけるとリロードしないときれいに動画が現れない
流れ
はじめにCardとかのhtmlをつくる。このときプロップスには何もない。そのあとサーバーから、videoのデータがとどく。

いったんデザインやる

layoutの{children}を持つ要素にw-fullを適用させる。じゃないと、変なところでwidthがきれる

ひらめいたかも。
client側とserver側を極限まで分ける。
//privateVideo.tsx (server側)
<div key={video.id} className={"bg-rose-500"}>
<Card video={video}/>
{/*<input type={"checkbox"} onChange={()=>handleCheck(video.videoSrc)}/>*/}
{/*<button onClick={onDelete(video.videoSrc)} type={"button"}>Delete</button>*/}
</div>
//card.tsx (まだserver側)
<div className={"flex flex-col items-center justify-center bg-green-500"}>
<iframe src={process.env.CDNURL + video.videoSrc} width={320} height={240}
allowFullScreen={true} loading={"eager"}/>
<h1 className={"text-2xl"}>{video.videoTitle}</h1>
<p>{video.description}</p>
<DeleteButton/> //(こいつがclient側)
</div>
//delte-button.tsx
<div>
<button>消す</button>
</div>
必要のないところまで、clientにしてた

Deleteボタンの中にshadcnUiを使用した。
Uploadのほうにも追加した。そのとき、server sideで回せるのではないかと思ってformActionとかでやってみたら、uploadはできるが、alertが出なかったのでやめた。useActionStateを使おうとしたが、React19がNext.jsではまだだったので無理だった。

upload押したら、画面の色が薄くなるようにする

dialogを使って背景を薄くする

thumbnailのuploadも作らないかん

別ファイル(use server)で定義した関数をクライアント側で呼び出してたけど、値が返ってこなかった。そのため、クライアント側でthumbnailをアップロードする処理を書いた。
const file =packInfo.thumbnail
const fileName=file.name.split('.').shift();
const fileExt=file.name.split('.').pop();
const filePath='${fileName}-${Math.random()}.${fileExt}`;
const {error:uploadError}=await supabase.storage.from("thumbnail").upload(filePath,file)
const packFront={
packTitle:packInfo.packTitle,
description:packInfo.description,
thumbnail:filePath,
userId:userId
}
await postPackFront(packFront)
if(uploadError){
throw uploadError
}

ロジック:ボタンを押す、packを作る(packIdを作成)、packIdをとってくる、packVideoを作成し、packIdをつける
const MyPack=await getPackId(userId)
if(!MyPack){
return;
}
const packId=MyPack.reverse()[0].id //最後に入ったpackIdを取り出している
for(const video of videos){
const file=video.file;
const fileName=file.name.split('.').shift();
const fileExt=file.name.split('.').pop();
const filePath=`${fileName}-${Math.random()}.${fileExt}`;
const {error:uploadError}=await supabase.storage.from('packVideo').upload(filePath,file);
const packVideoInfo={
videoTitle:vieo.videoTitle,
description:video.description,
file:filePath,
packId:packId
}
awiat postPackEachVideo(packVideoInfo);
if(uploadError){
throw uploadError;
}
}

packを作成する画面
同じ形の関数をコンパクトにする
const handleChange=(setter:React.Dispatch<React.SetStateAction<any>>,field:string)=>(newValue:string|File)=>{
setter((prevState:any)=>({...prevState,[field]:newValue}))
}
return(
<>
<PackHeader onPackTitleChange={handleChange(setPackInfo,'packTitle')} onDescriptionChange={handleChange(setPackInfo,'description')} onThumbnailChange={handleChange(setPackInfo,'thumbnail')}/>
</>
)
const PackHeader=()=>{
const handlePackTitleChange=(event:React.ChangeEvent<HTMLInputElement>)=>{
onPackTitleChange(event.target.value)
}
...
return(
<>
<input type="text" onChange={handlePackTitleChange}/>
...
</>
)
}

client側で、envファイルのキーを使うとき、NEXT_PUBLICを先頭につけなければいけない

flex-colを使うと、justify-centerとitems-centerの軸が反対になる

re-resizeをダウンロードしてユーザーがサイズを変更できるようにしている

next.jsのiframeのframeborderの設定ができない

・postVideoのページから、教材をアップロードできるようにする
・それぞれのlessonで表示させる

packInfoに教材:Fileを追加して、schemaとuserActionsを変更する

server actionには、plain objectとfew-built ins しか送れない

use clientないでpack-upload-buttonを作成した

customAlertを作成した。
export const CustomAlert=()=>{
return(
<div className={"fixed inset-0 flex items-center justify-center bg-black/80 z-50"}>
<div className="flex items-center flex-col gap-y-4 bg-white p-6 rounded-lg shadow-lg w-80">
<div>
{success?(
<Image src={"/happy-panda.jpg"} alt={""} width={150} height={150} className={"rounded-lg"}/>
):(
<Image src={"/sad-panda.jpg"} alt={""} width={150} height={150} className={"rounded-lg"}/>
)}
</div>
<p className={" text-3xl text-gray-700"}>
{message}
</p>
<Button variant={"default"} onClick={onClose} >Close</Button>
</div>
</div>
)
}

stripeを実装

import { NextResponse } from 'next/server';
import Stripe from 'stripe';
import { supabase } from '@/lib/supabaseClient';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2022-11-15',
});
export async function POST(request: Request) {
const { name, price, videoUrl } = await request.json();
try {
// Stripeで商品を作成
const product = await stripe.products.create({
name,
type: 'good',
metadata: {
videoUrl,
},
});
const stripePrice = await stripe.prices.create({
product: product.id,
unit_amount: price * 100,
currency: 'jpy',
});
// Supabaseに商品情報を保存
const { data, error } = await supabase
.from('products')
.insert({
name,
price,
video_url: videoUrl,
stripe_product_id: product.id,
stripe_price_id: stripePrice.id,
})
.select()
.single();
if (error) throw error;
return NextResponse.json({ productId: data.id, stripeProductId: product.id, stripePriceId: stripePrice.id });
} catch (error) {
console.error('Error creating product:', error);
return NextResponse.json({ error: 'Product creation failed' }, { status: 500 });
}
}
// app/components/ProductForm.tsx
'use client';
import { useState } from 'react';
export default function ProductForm() {
const [name, setName] = useState('');
const [price, setPrice] = useState('');
const [videoUrl, setVideoUrl] = useState('');
const [message, setMessage] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setMessage('商品を作成中...');
try {
const response = await fetch('/api/create-product', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, price: Number(price), videoUrl }),
});
const data = await response.json();
if (response.ok) {
setMessage('商品が正常に作成されました!');
setName('');
setPrice('');
setVideoUrl('');
} else {
setMessage(`エラー: ${data.error}`);
}
} catch (error) {
setMessage('商品の作成中にエラーが発生しました');
}
};
return (
<form onSubmit={handleSubmit}>
{/* 入力フィールドは前回と同じ */}
<button type="submit">商品を追加</button>
{message && <p>{message}</p>}
</form>
);
}

Error: Failed to parse src "undefinedthumbnail/Nagoya castle-w991jc.jpg" on next/image
, if using relative image it must start with a leading slash "/" or be an absolute URL (http:// or https://)
が発生
解決策:'use client'のときは、環境変数を使うためには、NEXT_PUBLIC_を付けなければいけない。

stripeの
const stripePrice=await stripe.prices.create({
product:product.id,
unit_amount:price*100,
currency:'usd',
})
unit_amountのところは、100倍することを忘れずに。でないと、priceがすくなすぎて怒られる。

stripeのwebhookを追加して、データベースに購入履歴を追加しようとしたが、webhookが実行されない。テストモードのエンドポイントの設定が間違っているのかもしれない。本番環境にした後にもう一度考える。とりあえず、全体的なデザインを行う

最終的に設定する

bcryptをnext.jsにimportするときは、
npm i --save-dev @types/bcrypt
を行って
ルートディレクトリにbcrypt.d.tsファイルを作成して
declare module 'bcrypt';

Google認証を導入したときは、
const {data:{session},error}=await supabase.auth.getSession()
if(error||!session){
redirect('/auth/login')
}
とすれば、普通の認証でも使える
編集後
上のコードは、セキュリティの関係上やめた方が良い
const supabase=createClient()
const {data,error}=await supabase.auth.getUser();
console.log(data.user?.user_metadata)
if(error||!data){
redirect("/auth/login")
}

tutorに金が入るように設定する。databaseのスキーマを組みなおす

反省
データベース設計しっかりとはじめにやるべきだった
後になっての変更大変

Module not found: Can't resolve 'net'
https://nextjs.org/docs/messages/module-not-found
Import trace for requested module:

ビルドエラー発生
> Build error occurred
Error: Failed to collect page data for /
解決策
action.tsでsupabaseのcreateClient()をグローバル変数として定義していたため起きた
それぞれの関数で、
const supabase=createClient()
をする

stripeのwebhookの実装をおこなった
import Stripe from "stripe";
import {updatePurchaseHistory} from "@/actions/userActtions";
import {NextApiRequest, NextApiResponse} from "next";
import {buffer} from 'micro'
import {redirect} from "next/navigation";
const stripe=new Stripe(process.env.STRIPE_SECRETKEY!)
const endpointSecret=process.env.STRIPE_WEBHOOK_SECRET!
export default async function handler(req:NextApiRequest,res:NextApiResponse){
if(req.method=='POST'){
const buf=await buffer(req); // バッファリング(一時的にメモリーにデータを貯めている。これにより、パフォーマンスが向上する)
const sig=req.headers['stripe-signature'] as string;
let event;
try{
event =stripe.webhooks.constructEvent(buf,sig,endpointSecret)
}catch(err:any){
console.error('Error verifying webhook signature:',err);
return res.status(400).send(`webhook Error: ${err.message}`);
}
const session=event.data.object as Stripe.Checkout.Session;
const userId=session.metadata?.userId;
const priceId=session.metadata?.priceId;
switch(event.type){
case 'checkout.session.completed':
try{
await updatePurchaseHistory(userId,priceId);
console.log('Purchase history updated successfully')
}catch(e){
console.error('Error updating purchase history:',e)
return res.status(500).json({e:'Failed to update purchase history'})
}
break;
case'invoice.payment_succeeded':
//ここにinvoice.payment_succeededイベントの処理
break;
default:console.log(`Unhandled event type:${event.type}`);
}
res.status(200).json({received:true})
}else{
res.setHeader('Allow','POST');
res.status(405).end('Method Not Allowed')
}
}
export const config={
api:{
bodyParser:false
}
}

map関数:ある配列の要素を加工して、新たな配列をつくる。
const list=[5,9,3,1,2,8,4,7,6,11,13,10,15,14,12]
const newList=list.map((item)=>{
return item+1
})
//[
6, 10, 4, 2, 3, 9,
5, 8, 7, 12, 14, 11,
16, 15, 13
]

sort:順番を変える。
const list=[
{id:2,
name:"a",
},
{id:1,
name:"b",
},
{id:4,
name:"c",
},
{id:3,
name:"d",
}
]
list.sort((a,b)=>b.id-a.id);
console.log(list)

stripeのwebhookようやくできた
NextResponseをつかうこと、webhook/route.tsにすることがポイント
import Stripe from "stripe";
import {headers} from "next/headers";
import {NextResponse} from "next/server"
const stripe=new Stripe(process.env.STRIPE_SECRET_KEY!)
const endpointSecret=process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(req:Request){
const body=await req.text();
const sig=headers().get('Stripe-Signature') as string;
let event:Stripe.Event;
try{
event=stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOKS_SECRET!
)
console.log('event',event)
}catch(err){
return new NextResponse("invalid signature",{status:400})
}
const session=event.data.object as Stripe.Checkout.Session;
console.log('session',session)
switch(event.type){
case 'checkout.session.completed':
//決済成功時にさせたい処理を書く
break;
case 'invoice.payment_succeeded':
//請求書の作成成功時の処理を書く
break;
default:console.log(`Unhandled event type:${event.type}`);//上のcaseに一致しなかったとき
}
return new NextResponse("ok",{status:200})
}

newとNextResponseについて調べておく

以下のエラー
⨯ Detected default export in 'C:\Users\torio\Desktop\Next.js\content-service\app\api\account\route.ts'. Export a named export for each HTTP method instead.
⨯ No HTTP methods exported in 'C:\Users\torio\Desktop\Next.js\content-service\app\api\account\route.ts'. Export a named export for each HTTP method.
//account
export async function POST(){
...
}
ページの一番上にあるコメントを消したら、解決した

webhookの中では、
const {data, error}=await supabase.auth.getUser()
は使えない

反省点:githubのcommit文を丁寧に書くこと

反省点:データベースを操作する関数、stripeに関する関数など、項目別に分けないと、後で脆弱性などについて確認する時に時間がかかる

try-catch文の中で、redirectをすると、redirectのときにerrorをthrowするので、redirectされない。そのため、try-catchの外で使う。

next.jsのimageはモジュールとして読み込もう

退会機能の作り方がわからない

stripeの税金を含めた支払いにするためには、checkout.session.createをいかのようにする。
ポイントは、
automatic_tax:{enabled:true},
customer:customerId,
custmer_update:{
address:"auto",
shipping:"auto"
}
stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price: priceId,
quantity: 1
},
],
mode: 'payment',
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/settlement-result/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/shop`,
automatic_tax: {enabled: true},
customer: customerId,
customer_update:{
address:"auto",
shipping:"auto"
},
payment_intent_data: {
application_fee_amount: 123,
transfer_data: {
destination: accountId
}
},
metadata: {
userId: userId,
priceId: priceId
},
invoice_creation: {
enabled: true,
invoice_data: {
description: '購入された商品となります',
metadata: {
userId: userId
}
}
},
},
);

squoosh.appを使って画像を最適化した

反省点
- 関数を細かく分ける
- 関数の名前を丁寧につける 後からでいいと考えがち(コンポーネントの名前も含む)
- レスポンシブデザインをしながら実装する
- コミット文にプレフィックスをつける
- figmaでデザインを決めておく
- データベース構造を決めておく
- 機能を決めておく
- コメントは消しておく
- 使っていないモジュールは消す
- バンドルサイズを考えてモジュールをインストールする
- エラーが発生したらまず仮説を立てる

bundle analyzerようやくできた。

radix uiで使っていないものを消去して、パフォーマンスを改善した

stripeの設定を本番に変更

学んだこと
- 反省点の内容
- 細かくコミットすること

今回のプロジェクトの目的
- 自分でドキュメントを読んで考えて制作すること
- Youtubeは最終手段。まずdocumentだけで理解する。
成果
- 自力で開発する土台を作ることができた
- 反省点を見つけることができた
- 開発の全体の流れを知ることができた
- SEOやパフォーマンスなど、コードを書く以外の技術を知ることができた