@c-time/frelio-cms 1.3.6 → 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/functions/api/auth/callback.ts +138 -0
- package/functions/api/storage/_middleware.ts +76 -0
- package/functions/api/storage/files/[uuid].ts +35 -0
- package/functions/api/storage/list.ts +19 -0
- package/functions/api/storage/rebuild/[year].ts +27 -0
- package/functions/api/storage/upload-set.ts +18 -0
- package/functions/api/storage/upload.ts +18 -0
- package/functions/api/storage/years.ts +18 -0
- package/package.json +6 -3
- package/workers/file-upload/package-lock.json +1606 -0
- package/workers/file-upload/package.json +14 -0
- package/workers/file-upload/src/controller/DIContainer.ts +103 -0
- package/workers/file-upload/src/controller/FileController.ts +248 -0
- package/workers/file-upload/src/domain/AuthEntity.ts +17 -0
- package/workers/file-upload/src/domain/FileEntity.ts +113 -0
- package/workers/file-upload/src/index.ts +134 -0
- package/workers/file-upload/src/infra/LoggerSingleton.ts +162 -0
- package/workers/file-upload/src/presenter/HttpPresenterImpl.ts +86 -0
- package/workers/file-upload/src/repository/AuthRepositoryPort.ts +11 -0
- package/workers/file-upload/src/repository/GitHubAuthRepositoryImpl.ts +58 -0
- package/workers/file-upload/src/repository/R2RepositoryImpl.ts +145 -0
- package/workers/file-upload/src/repository/R2RepositoryPort.ts +86 -0
- package/workers/file-upload/src/usecase/DeleteFileUseCase.ts +97 -0
- package/workers/file-upload/src/usecase/GetFileUseCase.ts +50 -0
- package/workers/file-upload/src/usecase/ListFilesUseCase.ts +52 -0
- package/workers/file-upload/src/usecase/RebuildIndexUseCase.ts +182 -0
- package/workers/file-upload/src/usecase/UpdateFileMetadataUseCase.ts +43 -0
- package/workers/file-upload/src/usecase/UploadFileUseCase.ts +62 -0
- package/workers/file-upload/src/usecase/UploadImageSetUseCase.ts +95 -0
- package/workers/file-upload/src/usecase/ValidateAuthUseCase.ts +26 -0
- package/workers/file-upload/tsconfig.json +13 -0
- package/workers/file-upload/wrangler.toml.example +11 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
interface Env {
|
|
2
|
+
GITHUB_CLIENT_ID?: string
|
|
3
|
+
GITHUB_CLIENT_SECRET: string
|
|
4
|
+
VITE_GITHUB_CLIENT_ID?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function errorHtml(title: string, message: string): string {
|
|
8
|
+
return `<!DOCTYPE html>
|
|
9
|
+
<html lang="ja">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="UTF-8">
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
13
|
+
<title>${title} - Frelio</title>
|
|
14
|
+
<style>
|
|
15
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
16
|
+
body {
|
|
17
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
18
|
+
background: #f5f5f5;
|
|
19
|
+
display: flex;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
align-items: center;
|
|
22
|
+
min-height: 100vh;
|
|
23
|
+
color: #333;
|
|
24
|
+
}
|
|
25
|
+
.card {
|
|
26
|
+
background: #fff;
|
|
27
|
+
border-radius: 12px;
|
|
28
|
+
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
|
29
|
+
padding: 40px;
|
|
30
|
+
max-width: 480px;
|
|
31
|
+
width: 90%;
|
|
32
|
+
text-align: center;
|
|
33
|
+
}
|
|
34
|
+
.icon {
|
|
35
|
+
font-size: 48px;
|
|
36
|
+
margin-bottom: 16px;
|
|
37
|
+
}
|
|
38
|
+
h1 {
|
|
39
|
+
font-size: 20px;
|
|
40
|
+
font-weight: 600;
|
|
41
|
+
margin-bottom: 12px;
|
|
42
|
+
}
|
|
43
|
+
p {
|
|
44
|
+
font-size: 14px;
|
|
45
|
+
color: #666;
|
|
46
|
+
line-height: 1.6;
|
|
47
|
+
margin-bottom: 24px;
|
|
48
|
+
}
|
|
49
|
+
a {
|
|
50
|
+
display: inline-block;
|
|
51
|
+
background: #1976d2;
|
|
52
|
+
color: #fff;
|
|
53
|
+
text-decoration: none;
|
|
54
|
+
padding: 10px 24px;
|
|
55
|
+
border-radius: 6px;
|
|
56
|
+
font-size: 14px;
|
|
57
|
+
font-weight: 500;
|
|
58
|
+
transition: background 0.2s;
|
|
59
|
+
}
|
|
60
|
+
a:hover { background: #1565c0; }
|
|
61
|
+
</style>
|
|
62
|
+
</head>
|
|
63
|
+
<body>
|
|
64
|
+
<div class="card">
|
|
65
|
+
<div class="icon">⚠️</div>
|
|
66
|
+
<h1>${title}</h1>
|
|
67
|
+
<p>${message}</p>
|
|
68
|
+
<a href="/">ダッシュボードに戻る</a>
|
|
69
|
+
</div>
|
|
70
|
+
</body>
|
|
71
|
+
</html>`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const onRequestGet: PagesFunction<Env> = async (context) => {
|
|
75
|
+
const url = new URL(context.request.url)
|
|
76
|
+
const code = url.searchParams.get('code')
|
|
77
|
+
|
|
78
|
+
if (!code) {
|
|
79
|
+
return new Response(
|
|
80
|
+
errorHtml('認証エラー', '認証コードが見つかりませんでした。ダッシュボードに戻って再度ログインしてください。'),
|
|
81
|
+
{ status: 400, headers: { 'Content-Type': 'text/html; charset=utf-8' } },
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const clientId =
|
|
86
|
+
context.env.GITHUB_CLIENT_ID || context.env.VITE_GITHUB_CLIENT_ID
|
|
87
|
+
|
|
88
|
+
if (!clientId) {
|
|
89
|
+
return new Response(
|
|
90
|
+
errorHtml('サーバー設定エラー', 'サーバーの設定に問題があります。管理者にお問い合わせください。'),
|
|
91
|
+
{ status: 500, headers: { 'Content-Type': 'text/html; charset=utf-8' } },
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const tokenResponse = await fetch(
|
|
97
|
+
'https://github.com/login/oauth/access_token',
|
|
98
|
+
{
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: {
|
|
101
|
+
Accept: 'application/json',
|
|
102
|
+
'Content-Type': 'application/json',
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
client_id: clientId,
|
|
106
|
+
client_secret: context.env.GITHUB_CLIENT_SECRET,
|
|
107
|
+
code,
|
|
108
|
+
}),
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const tokenData = (await tokenResponse.json()) as {
|
|
113
|
+
access_token?: string
|
|
114
|
+
error?: string
|
|
115
|
+
error_description?: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (tokenData.error) {
|
|
119
|
+
return new Response(
|
|
120
|
+
errorHtml(
|
|
121
|
+
'認証に失敗しました',
|
|
122
|
+
'認証コードが無効または期限切れです。ダッシュボードに戻ってブラウザをリロードし、再度ログインしてください。',
|
|
123
|
+
),
|
|
124
|
+
{ status: 400, headers: { 'Content-Type': 'text/html; charset=utf-8' } },
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Redirect to frontend callback with token
|
|
129
|
+
const origin = url.origin
|
|
130
|
+
const redirectUrl = `${origin}/auth/callback?token=${tokenData.access_token}`
|
|
131
|
+
return Response.redirect(redirectUrl, 302)
|
|
132
|
+
} catch {
|
|
133
|
+
return new Response(
|
|
134
|
+
errorHtml('認証エラー', '認証処理中にエラーが発生しました。ダッシュボードに戻って再度ログインしてください。'),
|
|
135
|
+
{ status: 500, headers: { 'Content-Type': 'text/html; charset=utf-8' } },
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage API Middleware - 認証 + CORS + DI
|
|
3
|
+
*
|
|
4
|
+
* /api/storage/* の全リクエストに対して実行される。
|
|
5
|
+
* FileController を組み立てて context.data に渡す。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { FileController } from '../../../../../workers/file-upload/src/controller/FileController'
|
|
9
|
+
import { AuthError } from '../../../../../workers/file-upload/src/domain/AuthEntity'
|
|
10
|
+
import { logger } from '../../../../../workers/file-upload/src/infra/LoggerSingleton'
|
|
11
|
+
import { HttpPresenterImpl } from '../../../../../workers/file-upload/src/presenter/HttpPresenterImpl'
|
|
12
|
+
import { GitHubAuthRepositoryImpl } from '../../../../../workers/file-upload/src/repository/GitHubAuthRepositoryImpl'
|
|
13
|
+
import { R2RepositoryImpl } from '../../../../../workers/file-upload/src/repository/R2RepositoryImpl'
|
|
14
|
+
import { ValidateAuthUseCase } from '../../../../../workers/file-upload/src/usecase/ValidateAuthUseCase'
|
|
15
|
+
|
|
16
|
+
interface Env {
|
|
17
|
+
R2: R2Bucket
|
|
18
|
+
R2_PUBLIC_URL: string
|
|
19
|
+
ALLOWED_ORIGINS?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type DataWithController = {
|
|
23
|
+
controller: FileController
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const onRequest: PagesFunction<Env, string, DataWithController> = async (context) => {
|
|
27
|
+
const requestId = crypto.randomUUID().slice(0, 8)
|
|
28
|
+
logger.setRequestId(requestId)
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// CORS ヘッダー構築
|
|
32
|
+
const origin = context.request.headers.get('Origin') ?? ''
|
|
33
|
+
const allowedOrigins = context.env.ALLOWED_ORIGINS?.split(',').map((s) => s.trim()) ?? []
|
|
34
|
+
// Pages Functions では同一オリジンも許可
|
|
35
|
+
const isSameOrigin = origin === new URL(context.request.url).origin
|
|
36
|
+
const matchedOrigin = isSameOrigin || allowedOrigins.includes(origin) ? origin : null
|
|
37
|
+
const corsHeaders = {
|
|
38
|
+
'Access-Control-Allow-Origin': matchedOrigin ?? '',
|
|
39
|
+
'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS',
|
|
40
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
41
|
+
Vary: 'Origin',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// CORS プリフライト
|
|
45
|
+
if (context.request.method === 'OPTIONS') {
|
|
46
|
+
return new Response(null, { headers: corsHeaders })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 認証
|
|
50
|
+
const authRepository = new GitHubAuthRepositoryImpl()
|
|
51
|
+
const validateAuth = new ValidateAuthUseCase(authRepository)
|
|
52
|
+
const presenter = new HttpPresenterImpl(corsHeaders)
|
|
53
|
+
try {
|
|
54
|
+
await validateAuth.execute(context.request.headers.get('Authorization'))
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error instanceof AuthError) {
|
|
57
|
+
return presenter.unauthorized(error.message)
|
|
58
|
+
}
|
|
59
|
+
throw error
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// DI: FileController を組み立てて context.data に渡す
|
|
63
|
+
const r2Repository = new R2RepositoryImpl(context.env.R2, context.env.R2_PUBLIC_URL)
|
|
64
|
+
context.data.controller = new FileController(r2Repository, corsHeaders)
|
|
65
|
+
|
|
66
|
+
return await context.next()
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logger.error('StorageMiddleware', 'Unhandled error', { error })
|
|
69
|
+
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
|
70
|
+
status: 500,
|
|
71
|
+
headers: { 'Content-Type': 'application/json' },
|
|
72
|
+
})
|
|
73
|
+
} finally {
|
|
74
|
+
logger.clearRequestId()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /api/storage/files/:uuid
|
|
3
|
+
* GET - ファイル情報
|
|
4
|
+
* PATCH - メタデータ更新
|
|
5
|
+
* DELETE - ファイル削除
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FileController } from '../../../../../../workers/file-upload/src/controller/FileController'
|
|
9
|
+
|
|
10
|
+
interface Env {
|
|
11
|
+
R2: R2Bucket
|
|
12
|
+
R2_PUBLIC_URL: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type Data = {
|
|
16
|
+
controller: FileController
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type Params = 'uuid'
|
|
20
|
+
|
|
21
|
+
export const onRequestGet: PagesFunction<Env, Params, Data> = async (context) => {
|
|
22
|
+
const uuid = context.params.uuid as string
|
|
23
|
+
return context.data.controller.handleGetFile(uuid)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const onRequestPatch: PagesFunction<Env, Params, Data> = async (context) => {
|
|
27
|
+
const uuid = context.params.uuid as string
|
|
28
|
+
return context.data.controller.handleUpdateFileMetadata(uuid, context.request)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const onRequestDelete: PagesFunction<Env, Params, Data> = async (context) => {
|
|
32
|
+
const uuid = context.params.uuid as string
|
|
33
|
+
const url = new URL(context.request.url)
|
|
34
|
+
return context.data.controller.handleDeleteFile(uuid, url)
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/storage/list - ファイル一覧
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FileController } from '../../../../../workers/file-upload/src/controller/FileController'
|
|
6
|
+
|
|
7
|
+
interface Env {
|
|
8
|
+
R2: R2Bucket
|
|
9
|
+
R2_PUBLIC_URL: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type Data = {
|
|
13
|
+
controller: FileController
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const onRequestGet: PagesFunction<Env, string, Data> = async (context) => {
|
|
17
|
+
const url = new URL(context.request.url)
|
|
18
|
+
return context.data.controller.handleListFiles(url)
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/storage/rebuild/:year - インデックス再構築
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FileController } from '../../../../../../workers/file-upload/src/controller/FileController'
|
|
6
|
+
|
|
7
|
+
interface Env {
|
|
8
|
+
R2: R2Bucket
|
|
9
|
+
R2_PUBLIC_URL: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type Data = {
|
|
13
|
+
controller: FileController
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type Params = 'year'
|
|
17
|
+
|
|
18
|
+
export const onRequestPost: PagesFunction<Env, Params, Data> = async (context) => {
|
|
19
|
+
const year = parseInt(context.params.year as string)
|
|
20
|
+
if (isNaN(year)) {
|
|
21
|
+
return new Response(JSON.stringify({ error: 'Invalid year' }), {
|
|
22
|
+
status: 400,
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
return context.data.controller.handleRebuildIndex(year)
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/storage/upload-set - 画像セットのアップロード
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FileController } from '../../../../../workers/file-upload/src/controller/FileController'
|
|
6
|
+
|
|
7
|
+
interface Env {
|
|
8
|
+
R2: R2Bucket
|
|
9
|
+
R2_PUBLIC_URL: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type Data = {
|
|
13
|
+
controller: FileController
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const onRequestPost: PagesFunction<Env, string, Data> = async (context) => {
|
|
17
|
+
return context.data.controller.handleUploadSet(context.request, context)
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/storage/upload - 単一ファイルのアップロード
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FileController } from '../../../../../workers/file-upload/src/controller/FileController'
|
|
6
|
+
|
|
7
|
+
interface Env {
|
|
8
|
+
R2: R2Bucket
|
|
9
|
+
R2_PUBLIC_URL: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type Data = {
|
|
13
|
+
controller: FileController
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const onRequestPost: PagesFunction<Env, string, Data> = async (context) => {
|
|
17
|
+
return context.data.controller.handleUpload(context.request)
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/storage/years - 利用可能な年一覧
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FileController } from '../../../../../workers/file-upload/src/controller/FileController'
|
|
6
|
+
|
|
7
|
+
interface Env {
|
|
8
|
+
R2: R2Bucket
|
|
9
|
+
R2_PUBLIC_URL: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type Data = {
|
|
13
|
+
controller: FileController
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const onRequestGet: PagesFunction<Env, string, Data> = async (context) => {
|
|
17
|
+
return context.data.controller.handleGetYears()
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c-time/frelio-cms",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.7",
|
|
4
4
|
"description": "Frelio CMS Admin - pre-built static bundle for 1-repo setup",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -12,14 +12,17 @@
|
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
-
"dist"
|
|
15
|
+
"dist",
|
|
16
|
+
"functions",
|
|
17
|
+
"workers"
|
|
16
18
|
],
|
|
17
19
|
"type": "module",
|
|
18
20
|
"scripts": {
|
|
19
21
|
"dev": "vite",
|
|
20
22
|
"dev:full": "wrangler pages dev --compatibility-date=2024-01-01 -- vite",
|
|
21
23
|
"build": "vite build",
|
|
22
|
-
"prepublishOnly": "vite build",
|
|
24
|
+
"prepublishOnly": "vite build && cp -r ../../workers ./workers",
|
|
25
|
+
"postpublish": "rm -rf ./workers",
|
|
23
26
|
"preview": "vite preview",
|
|
24
27
|
"typecheck": "tsc --noEmit",
|
|
25
28
|
"test": "vitest",
|