Compare commits

...

3 Commits

Author SHA1 Message Date
posimai 276ae2dc9d chore(status): add mai pending tasks section (Firebase key, Stripe, etc.)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 09:55:54 +09:00
posimai 1eb94565d5 fix(together): add member auth to GET endpoints, remove comment_count JOIN 2026-04-18 08:51:16 +09:00
posimai 8f41c4736f feat(together): add 'read' to reaction type allowlist 2026-04-17 23:37:22 +09:00
2 changed files with 68 additions and 12 deletions

View File

@ -31,6 +31,17 @@ node_modules/.bin/tauri build
# → target/release/bundle/msi/Posimai Guard_0.1.0_x64_en-US.msi # → target/release/bundle/msi/Posimai Guard_0.1.0_x64_en-US.msi
``` ```
### MCP 設定mai 作業)
1. **Stripe キー確認・ローテーション**
- Stripe ダッシュボード → Developers → Logs で 4/11 の API リクエストを確認(使用元の特定)
- 既存の `sk_test_...` キーをロールオーバー(無効化)
- 新しいテスト用シークレットキーを発行(名前: `posimai-mcp`
- [.mcp.json](.mcp.json) の `FILL_IN: Stripe ダッシュボードの sk_test_...` を置換
2. **Vercel トークン発行**
- [vercel.com/account/tokens](https://vercel.com/account/tokens) → Create → 名前: `claude-code-mcp`、Scope: Full Account
- [.mcp.json](.mcp.json) の `FILL_IN: vercel.com/account/tokens...` を置換
3. **Claude Code 再起動** → vercel・stripe MCP が有効になる
### ビジネス化 ### ビジネス化
1. **Eiji に Stripe sandbox テストをお願いする**(購入フロー確認) 1. **Eiji に Stripe sandbox テストをお願いする**(購入フロー確認)
2. **日本酒アプリを完成させて展開**mai 最優先) 2. **日本酒アプリを完成させて展開**mai 最優先)
@ -38,6 +49,15 @@ node_modules/.bin/tauri build
4. Store デザイン確定Eiji と A/B/C/D から選定) 4. Store デザイン確定Eiji と A/B/C/D から選定)
5. Stripe 本番モード切り替え(上記完了後) 5. Stripe 本番モード切り替え(上記完了後)
## mai 作業待ちAI では実行できない)
| タスク | 内容 | 優先度 |
|--------|------|--------|
| **article-keeper Firebase キー削除** | Firebase Console でプロジェクトごと削除(不要なら)。完了後に AI が article-keeper/ ディレクトリを削除・コミットする | 高(キーが GitHub 公開履歴に存在) |
| Stripe sandbox テスト | Eiji に購入フロー確認依頼 | 中 |
| 特商法ページ記入 | 事業者名・住所・電話番号 | 中 |
| Store デザイン確定 | Eiji と A/B/C/D から選定 | 中 |
## ブロック中 ## ブロック中
| ブロック | 待ち先 | | ブロック | 待ち先 |

View File

@ -2481,9 +2481,17 @@ ${excerpt}
} }
}); });
// GET /together/members/:groupId — メンバー一覧 // GET /together/members/:groupId — メンバー一覧 ?u=username
r.get('/together/members/:groupId', async (req, res) => { r.get('/together/members/:groupId', async (req, res) => {
const username = req.query.u;
if (!username) return res.status(400).json({ error: 'u (username) は必須です' });
try { try {
const memberCheck = await pool.query(
'SELECT 1 FROM together_members WHERE group_id=$1 AND username=$2',
[req.params.groupId, username]
);
if (memberCheck.rows.length === 0) return res.status(403).json({ error: 'グループのメンバーではありません' });
const result = await pool.query( const result = await pool.query(
'SELECT username, joined_at FROM together_members WHERE group_id=$1 ORDER BY joined_at', 'SELECT username, joined_at FROM together_members WHERE group_id=$1 ORDER BY joined_at',
[req.params.groupId] [req.params.groupId]
@ -2546,12 +2554,20 @@ ${excerpt}
} }
}); });
// GET /together/feed/:groupId — フィード(リアクション・コメント数付き) // GET /together/feed/:groupId — フィード(リアクション付き)
// ?limit=N&cursor=<ISO timestamp> でカーソルページネーション対応 // ?u=username&limit=N&cursor=<ISO timestamp>
r.get('/together/feed/:groupId', async (req, res) => { r.get('/together/feed/:groupId', async (req, res) => {
const username = req.query.u;
if (!username) return res.status(400).json({ error: 'u (username) は必須です' });
try { try {
const memberCheck = await pool.query(
'SELECT 1 FROM together_members WHERE group_id=$1 AND username=$2',
[req.params.groupId, username]
);
if (memberCheck.rows.length === 0) return res.status(403).json({ error: 'グループのメンバーではありません' });
const limit = Math.min(parseInt(req.query.limit) || 20, 50); const limit = Math.min(parseInt(req.query.limit) || 20, 50);
const cursor = req.query.cursor; // shared_at の ISO タイムスタンプ const cursor = req.query.cursor;
const params = [req.params.groupId]; const params = [req.params.groupId];
let cursorClause = ''; let cursorClause = '';
if (cursor) { if (cursor) {
@ -2564,11 +2580,9 @@ ${excerpt}
COALESCE( COALESCE(
json_agg(DISTINCT jsonb_build_object('username', r.username, 'type', r.type)) json_agg(DISTINCT jsonb_build_object('username', r.username, 'type', r.type))
FILTER (WHERE r.username IS NOT NULL), '[]' FILTER (WHERE r.username IS NOT NULL), '[]'
) AS reactions, ) AS reactions
COUNT(DISTINCT c.id)::int AS comment_count
FROM together_shares s FROM together_shares s
LEFT JOIN together_reactions r ON r.share_id = s.id LEFT JOIN together_reactions r ON r.share_id = s.id
LEFT JOIN together_comments c ON c.share_id = s.id
WHERE s.group_id = $1 ${cursorClause} WHERE s.group_id = $1 ${cursorClause}
GROUP BY s.id GROUP BY s.id
ORDER BY s.shared_at DESC ORDER BY s.shared_at DESC
@ -2585,9 +2599,17 @@ ${excerpt}
} }
}); });
// GET /together/article/:shareId — アーカイブ本文取得 // GET /together/article/:shareId — アーカイブ本文取得 ?u=username
r.get('/together/article/:shareId', async (req, res) => { r.get('/together/article/:shareId', async (req, res) => {
const username = req.query.u;
if (!username) return res.status(400).json({ error: 'u (username) は必須です' });
try { try {
const memberCheck = await pool.query(
'SELECT 1 FROM together_members m JOIN together_shares s ON s.group_id=m.group_id WHERE s.id=$1 AND m.username=$2',
[req.params.shareId, username]
);
if (memberCheck.rows.length === 0) return res.status(403).json({ error: 'グループのメンバーではありません' });
const result = await pool.query( const result = await pool.query(
'SELECT id, title, url, full_content, summary, archive_status, shared_at FROM together_shares WHERE id=$1', 'SELECT id, title, url, full_content, summary, archive_status, shared_at FROM together_shares WHERE id=$1',
[req.params.shareId] [req.params.shareId]
@ -2603,7 +2625,7 @@ ${excerpt}
r.post('/together/react', async (req, res) => { r.post('/together/react', async (req, res) => {
const { share_id, username, type = 'like' } = req.body || {}; const { share_id, username, type = 'like' } = req.body || {};
if (!share_id || !username) return res.status(400).json({ error: 'share_id と username は必須です' }); if (!share_id || !username) return res.status(400).json({ error: 'share_id と username は必須です' });
if (!['like', 'star', 'fire'].includes(type)) return res.status(400).json({ error: 'type は like/star/fire のみ有効です' }); if (!['like', 'star', 'fire', 'read'].includes(type)) return res.status(400).json({ error: 'type は like/star/fire/read のみ有効です' });
try { try {
// share の group に対してメンバーであることを確認 // share の group に対してメンバーであることを確認
const memberCheck = await pool.query( const memberCheck = await pool.query(
@ -2635,9 +2657,17 @@ ${excerpt}
} }
}); });
// GET /together/comments/:shareId — コメント一覧 // GET /together/comments/:shareId — コメント一覧 ?u=username
r.get('/together/comments/:shareId', async (req, res) => { r.get('/together/comments/:shareId', async (req, res) => {
const username = req.query.u;
if (!username) return res.status(400).json({ error: 'u (username) は必須です' });
try { try {
const memberCheck = await pool.query(
'SELECT 1 FROM together_members m JOIN together_shares s ON s.group_id=m.group_id WHERE s.id=$1 AND m.username=$2',
[req.params.shareId, username]
);
if (memberCheck.rows.length === 0) return res.status(403).json({ error: 'グループのメンバーではありません' });
const result = await pool.query( const result = await pool.query(
'SELECT * FROM together_comments WHERE share_id=$1 ORDER BY created_at', 'SELECT * FROM together_comments WHERE share_id=$1 ORDER BY created_at',
[req.params.shareId] [req.params.shareId]
@ -2673,9 +2703,15 @@ ${excerpt}
} }
}); });
// GET /together/search/:groupId — キーワード / タグ検索 // GET /together/search/:groupId — キーワード / タグ検索 ?u=username
r.get('/together/search/:groupId', async (req, res) => { r.get('/together/search/:groupId', async (req, res) => {
const { q = '', tag = '' } = req.query; const { q = '', tag = '', u: username = '' } = req.query;
if (!username) return res.status(400).json({ error: 'u (username) は必須です' });
const memberOk = await pool.query(
'SELECT 1 FROM together_members WHERE group_id=$1 AND username=$2',
[req.params.groupId, username]
).then(r => r.rows.length > 0).catch(() => false);
if (!memberOk) return res.status(403).json({ error: 'グループのメンバーではありません' });
if (!q && !tag) return res.status(400).json({ error: 'q または tag が必要です' }); if (!q && !tag) return res.status(400).json({ error: 'q または tag が必要です' });
try { try {
const keyword = q ? `%${q}%` : ''; const keyword = q ? `%${q}%` : '';