æåŸ ã³ãŒãå ±ææ²ç€ºæ¿ããçŸéå ±é ¬ãªããã§èšèšããçç±
Claude Codeã®èšæ¶ã4å±€ã«åãã話ããç¶ããå人éçºãéç£ããããã®èªåååºç€ãã·ãªãŒãºã§ããä»åã¯ãæåŸ ã³ãŒãå ±ææ²ç€ºæ¿ InviteLoopïŒæ¬çªçšŒåäžïŒinviteloop.vercel.appïŒãèšèšãããšãã«æåã«æ±ºããæ¹éããå ±é ¬ã¯å ¬åŒç¹å žã®ã¿ãçŸéã®ãããšãã¯äžåæé€ããã ãšããéžæã®èæ¯ãšããã®å€æãã³ãŒãã«èœãšã蟌ããŸã§ã®è©±ãæžããŸãã
InviteLoop ãšã¯
InviteLoop ã¯ãåçš®ãµãŒãã¹ã®æåŸ ã³ãŒãããŠãŒã¶ãŒãæçš¿ã»å ±æã§ããæ²ç€ºæ¿ã§ããDropbox ã Uber ã®ãããªã玹ä»ãããšââããããããå ¬åŒããã°ã©ã ãæã€ãµãŒãã¹ã察象ã«ãæåŸ ã³ãŒããäžãæã«éããŠæ¢ããããããããšãç®çã«ããŠããŸãã
æè¡ã¹ã¿ãã¯ã¯æ¬¡ã®ãšããã§ãã
| ã¬ã€ã€ãŒ | æ¡çšæè¡ |
|---|---|
| ãã¬ãŒã ã¯ãŒã¯ | Next.js 16.2.9ïŒApp RouterïŒ |
| ããŒã¿ããŒã¹ | TursoïŒlibsql / SQLite äºæïŒ |
| ORM | Drizzle ORM |
| èªèšŒ | NextAuth.js v5ïŒGoogle OAuthïŒ |
| ã¹ã¿ã€ãªã³ã° | Tailwind CSS v4 |
| ãã¹ã | Vitest + Playwright |
| ããã〠| Vercel |
äžèŠã·ã³ãã«ã«èŠããŸãããèšèšã§ãã£ãšãæéã䜿ã£ãã®ã¯UI ãæ©èœããããå ±é ¬ã¢ãã«ãã©ãã«åŒããããšããäžç¹ã§ããã
ãªããçŸéå ±é ¬ãªããã«ããã®ã
æåŸ ã³ãŒãæ²ç€ºæ¿ãäœãããšãããšãèªç¶ã«æµ®ãã¶ã¢ã€ãã¢ããã³ãŒãã䜿ãããããã€ã³ããä»äžããŠæéã§ããããã«ãããããšãããã®ã§ããå®éãæµ·å€ã«ã¯åæ§ã®ã¢ãã«ãæã€ãµãŒãã¹ãååšããŸããããã§ãç§ãçŸéå ±é ¬ãæé€ããçç±ã¯ã倧ããäžã€ã§ãã
çç±1ïŒåãµãŒãã¹ã®å©çšèŠçŽãªã¹ã¯
æåŸ ããã°ã©ã ãæäŸããŠãããµãŒãã¹ã¯ããããŠãèŠçŽã®äžã«ãæåŸ ç¹å žã®è»¢å£²ã»æéãçŠæ¢ãããã第äžè ãã©ãããã©ãŒã ãä»ããç¹å žã®åé åžãçŠããããšãã£ãæ¡é ãæã£ãŠããŸããInviteLoop ããã³ãŒãã䜿ã£ããâåããšããä»çµã¿ãæã€ãšããããåªä»ããŠãŠãŒã¶ãŒãåãµãŒãã¹ã®èŠçŽã鿥çã«ç Žãå¯èœæ§ãçãŸããŸãã
ãã©ãããã©ãŒã åŽã«ã¯ããŠãŒã¶ãŒã®èŠçŽéåãå©é·ãã責任ãåããããªã¹ã¯ããããŸãããç¥ããªãã£ããã§ã¯æžãŸãªãã±ãŒã¹ã«ãªããããããèšè𿮵éã§çŸéã®æµããå®å šã«åãããšã«ããŸããã
çç±2ïŒã¹ãã ã»æ°Žå¢ãæçš¿ã®èªçº
çŸéå ±é ¬ããããšãã䜿ãããåæ°ã«å¿ããŠåçãåºãããšããæ§é ã«ãªããŸãããããšãåäžãŠãŒã¶ãŒãè€æ°ã¢ã«ãŠã³ãã䜿ã£ãŠèªåã®ã³ãŒãã䜿çšå ±åããããããã¯ãããã§å€§éæçš¿ããã€ã³ã»ã³ãã£ããçãŸããŸãã
ã¹ãã 察çã¯åŸä»ãã§è¿œå ãããããã€ã³ã»ã³ãã£ãèšèšã®æ®µéã§å°ããæ¹ãã¯ããã«æ¥œã§ããééçãªåæ©ãæ¶ãããšã§ãäžæ£è¡çºã®ãªã¿ãŒã³ããŒãã«ããŸããã
çç±3ïŒã³ãã¥ããã£ã®å¥å šæ§
æåŸ ã³ãŒãæ²ç€ºæ¿ã«æåŸ ãããäœéšã¯ã䜿ããã³ãŒããçŽ æ©ãèŠã€ããããããšãã§ããçŸéã絡ããšãæçš¿è ããã§ããã ãå€ã䜿ãããã³ãŒãããåªå ããŠå質ããæ°ã远ããããããã«ãªããŸããããã§ããå質ãšã¯ãå®éã«æå¹ãªã³ãŒãã§ããããšããããŒããäžå¯§ã§äœ¿ãæ¹ãåããããšããæããŸããå ±é ¬ã¯å ¬åŒç¹å žã ãã«ããããšã§ããæ¬åœã«äœ¿ããã³ãŒããå ±æãããããšããã¢ãããŒã·ã§ã³ä»¥å€ãå ¥ãã«ããèšèšã«ããŸããã
ã¹ããŒãã§ãå ±é ¬ããæåããååšãããªã
èšèšå€æãã³ãŒãã«èœãšã蟌ãæåã®äžæã¯ãDB ã¹ããŒãã«å ±é ¬ã®æŠå¿µãäžåæžããªãããšã§ããã
// src/db/schema.tsïŒæç²ïŒ
export const services = sqliteTable('service', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text('name').notNull(),
slug: text('slug').notNull().unique(),
category: text('category').notNull(),
perkSummary: text('perk_summary').notNull().default(''),
perkJa: text('perk_ja').notNull().default(''),
perkEn: text('perk_en').notNull().default(''),
officialUrl: text('official_url').notNull().default(''),
createdAt: integer('created_at', { mode: 'timestamp_ms' })
.notNull().$defaultFn(() => new Date()),
})
perkJa / perkEn ã¯ããã®ãµãŒãã¹ã®å
¬åŒç¹å
žã¯ããã§ãããšãã説æãã£ãŒã«ãã§ããéé¡ãå
¥ããããšã¯ãããŸãããããã¯ãããŸã§å
¬åŒãå
¬è¡šããŠããæ
å ±ããã®ãŸãŸè¡šç€ºããããã®ãã®ãInviteLoop ç¬èªã®ãã€ã³ãæ®é«ãæéããŒãã«ã¯ã¹ããŒãã®ã©ãã«ãååšããŸããã
æåŸ ã³ãŒãæ¬äœã®ããŒãã«ãèŠãŠããå ±é ¬ã«é¢ããã«ã©ã ã¯ãŒãã§ãã
export const inviteCodes = sqliteTable('invite_code', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
serviceId: text('service_id').notNull().references(() => services.id, { onDelete: 'cascade' }),
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
code: text('code').notNull(),
url: text('url').notNull().default(''),
note: text('note').notNull().default(''),
status: text('status', { enum: ['active', 'hidden', 'removed'] }).notNull().default('active'),
clickCount: integer('click_count').notNull().default(0),
upvotes: integer('upvotes').notNull().default(0),
usageCount: integer('usage_count').notNull().default(0),
reportCount: integer('report_count').notNull().default(0),
createdAt: integer('created_at', { mode: 'timestamp_ms' })
.notNull().$defaultFn(() => new Date()),
})
clickCountã»upvotesã»usageCountã»reportCount ã¯ã©ãããæ²ç€ºæ¿ãšããŠã®ä¿¡é ŒåºŠã»äººæ°åºŠãç€ºãææšãã§ãããæéã®æ ¹æ ã«ã¯ãªããŸãããã¹ããŒããçå®ã®åäžãœãŒã¹ã§ãã以äžãååšããªãã«ã©ã ããã¯äœãæããŸããããããèšèšäžã®æåŒ·ã®æ
ä¿ã§ãã
æçš¿å¶éãš URL ãã¡ã€ã³æ€èšŒ
ãå ±é ¬ããªããã°ä¹±çšãããªããïŒããšåããããšãæ£çŽãŒãã§ã¯ãããŸãããèªåã®ã³ãŒããäžäœã«è¡šç€ºããããããããã¯åãªãããããã§å€§éæçš¿ããã±ãŒã¹ã¯èããããŸãããããé²ãã®ãæçš¿ã¬ãŒãå¶éãšURLæ€èšŒã§ãã
// src/lib/rate-limit.ts
export const MAX_POSTS_PER_DAY = 5
export function checkRateLimit(i: RateLimitInput): RateLimitResult {
if (i.alreadyPostedThisService) return { ok: false, reason: 'duplicate_service' }
if (i.postsToday >= i.maxPerDay) return { ok: false, reason: 'daily_limit' }
return { ok: true }
}
1ãŠãŒã¶ãŒãåããµãŒãã¹ã«è€æ°ã®ã³ãŒããæçš¿ããããšã¯çŠæ¢ãããŠããŸãïŒduplicate_serviceïŒããŸãã1æ¥ã®æçš¿äžéã¯5ä»¶ïŒMAX_POSTS_PER_DAY = 5ïŒã§ãã
æçš¿ãã©ãŒã ã«ã¯ãã³ãŒããšã»ããã§æåŸ URLã貌ããããã£ãŒã«ãããããŸãããããã«ä»»æã®URLãå ¥ãããããšå¥ãµãŒãã¹ãžã®èªå°ã«äœ¿ãããŸããããã§ãURL ã¯åœè©²ãµãŒãã¹ã®å ¬åŒãã¡ã€ã³ã®ã¿èš±å¯ããä»çµã¿ã«ããŸããã
// src/lib/allowed-hosts.ts
export function allowedHostsFor(officialUrl: string): string[] {
if (!officialUrl) return []
try {
const h = new URL(officialUrl).host
return h.startsWith('www.') ? [h, h.slice(4)] : [h, `www.${h}`]
} catch { return [] }
}
officialUrl ãã www ãã/ãªã ã®äž¡æ¹ãèš±å¯ãã¹ããšããŠçæããvalidateSubmission ã®äžã§ URL ãæ€èšŒããŸãã
// src/lib/validation.tsïŒæç²ïŒ
if (parsed.protocol !== 'https:' || !allowedHosts.includes(parsed.host)) {
return { ok: false, error: 'url_not_allowed' }
}
https: éå®ãã€å
¬åŒãã¡ã€ã³éå®ãããã ãã§å€éšãªã³ã¯ã䜿ã£ããã£ãã·ã³ã°çãªäœ¿ãæ¹ãã»ãŒå°ããããŸãã
èªåã¢ãã¬ãŒã·ã§ã³ïŒ3 ä»¶ã¬ããŒãã§ãªãŒããã€ã
ãŠãŒã¶ãŒããã®éå ±ãåããŠãäžå®æ°ã«éããã³ãŒãã¯èªåçã«é衚瀺ã«ããŸãã
// src/lib/scoring.ts
export const REPORT_AUTOHIDE_THRESHOLD = 3
// src/repos/engagement.tsïŒreportCode 颿°ã®æ«å°ŸïŒ
const reportCount = rows[0].reportCount
const hidden = reportCount >= REPORT_AUTOHIDE_THRESHOLD
if (hidden) {
await db.update(inviteCodes)
.set({ status: 'hidden' })
.where(eq(inviteCodes.id, codeId))
}
return { ok: true as const, reportCount, hidden }
3ä»¶ã®éå ±ã§ status ã 'hidden' ã«ãªããŸããäžèЧç»é¢ã§ã¯ãã®ã¹ããŒã¿ã¹ãæã€ã³ãŒããé€å€ãããããåé¡ã®ããæçš¿ã¯äººæãä»ããã«é衚瀺ã«ãªããŸããéé²ã«ããŒãã«ãäžãããšæ£åžžãªã³ãŒããŸã§å·»ã蟌ãŸããã®ã§ãéŸå€ã¯çŸæç¹ã§ã¯æ§ãããª3ä»¶ã§ãã
åäžãŠãŒã¶ãŒãåãã³ãŒããè€æ°åéå ±ã§ããªããããreport ããŒãã«ã« (code_id, reporter_user_id) ã®è€åãŠããŒã¯å¶çŽãèšããŠããŸãã
// src/db/schema.tsïŒæç²ïŒ
export const reports = sqliteTable('report', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
codeId: text('code_id').notNull().references(() => inviteCodes.id, { onDelete: 'cascade' }),
reporterUserId: text('reporter_user_id'),
reason: text('reason').notNull().default(''),
createdAt: integer('created_at', { mode: 'timestamp_ms' })
.notNull().$defaultFn(() => new Date()),
}, (t) => ({ uniq: uniqueIndex('report_code_user_uq').on(t.codeId, t.reporterUserId) }))
ãã§ã¢ãªäžŠã³é ïŒã¹ã³ã¢Ãå éã·ã£ããã«
æçš¿æ°ãå¢ããŠãããšãã©ã®é çªã§ã³ãŒãã䞊ã¹ãããã UX ã®èŠã«ãªããŸããçŽç²ã«ã¹ã³ã¢é ïŒäººæ°é ïŒã§äžŠã¹ããšãå€ãããäžäœã«ããã³ãŒããæå©ã«ãªããããŸããéã«æ°çé ã ãšãããã«åãããŠæç¥šãåãããã£ã³ã¹ããªããªããŸãã
ãã®åé¡ã解決ããããã«ãå é確çã·ã£ããã«ããæ¡çšããŸããã
// src/lib/scoring.ts
export const SCORE_CLICK_WEIGHT = 1
export const SCORE_USAGE_WEIGHT = 5
export const SCORE_UPVOTE_WEIGHT = 3
export function computeScore(input: {
clickCount: number
usageCount: number
upvoteCount: number
}): number {
return (
input.clickCount * SCORE_CLICK_WEIGHT +
input.usageCount * SCORE_USAGE_WEIGHT +
input.upvoteCount * SCORE_UPVOTE_WEIGHT
)
}
ãå®éã«äœ¿çšããïŒusageCountïŒããæãéãè©äŸ¡ãïŒÃ5ïŒãããããïŒupvotesïŒããç¶ãïŒÃ3ïŒããã¯ãªãã¯ããã ãïŒclickCountïŒããæã軜ãïŒÃ1ïŒãšããéã¿ä»ãã§ãã
䞊ã³é ã®èšç®ã¯ computeFairOrder ãæ
ããŸãã
// src/lib/ordering.tsïŒæç²ïŒ
export function computeFairOrder<T extends Orderable>(
codes: T[],
opts: { newQuota: number; seed: number; now: number; freshWindowMs: number },
): T[] {
const fresh = codes
.filter((x) => opts.now - x.createdAt < opts.freshWindowMs)
.sort((a, b) => a.createdAt - b.createdAt)
const reserved = fresh.slice(0, Math.max(0, opts.newQuota))
const reservedIds = new Set(reserved.map((x) => x.id))
const rest = codes.filter((x) => !reservedIds.has(x.id))
const weight = (x: T) => 1 + Math.min(x.score * ORDER_SCORE_WEIGHT, ORDER_MAX_BOOST)
return [...reserved, ...weightedSeededShuffle(rest, weight, opts.seed)]
}
çŽè¿7æ¥éïŒFRESH_WINDOW_MSïŒã®æ°çã³ãŒããäžå®æ ã§å
é 確ä¿ããæ®ããã¹ã³ã¢ã«å¿ãã確çã§äžŠã¹ãŸãã確çã®ä¹±æ°ã¯ mulberry32 ã·ãŒãã§åºå®ãããŠãããåãã·ãŒããäžãããšåãé ã«ãªããŸãïŒç»é¢ãªããŒãã®ãã³ã«ãã©ãã©ã«ãªããªãïŒã
ãåãåãããšéç¥ã®èšèš
æ²ç€ºæ¿ç³»ã®ãµãŒãã¹ã¯åãåãã察å¿ããããããšå¢ããŸããå€éšãµãŒãã¹ã远å ããããšãªããTurso ã®æ¢å DB ã«ä¿åããèšèšã«ããŸããã
// src/db/schema.tsïŒæç²ïŒ
export const contactMessages = sqliteTable('contact_message', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text('name').notNull(),
email: text('email').notNull(),
body: text('body').notNull(),
createdAt: integer('created_at', { mode: 'timestamp_ms' })
.notNull().$defaultFn(() => new Date()),
// Discordéç¥æžã¿ãïŒ0=æªéç¥ïŒãçä¿¡æã«éç¥ã詊ã¿ã倱æåã¯cronãååããã
notified: integer('notified').notNull().default(0),
})
ãåãåãããå±ããšããŸã DB ã«ä¿åããŠãã Discord ã® Webhook ã«éç¥ãæããŸãã
// src/app/api/contact/route.tsïŒæç²ïŒ
const id = await createContactMessage(db, v.value)
// çä¿¡ãå³ Discord éç¥ã倱æããŠãä¿åã¯å®ãïŒcron ãåŸã§ååïŒã
const ok = await notifyContact({
project: 'InviteLoop',
id,
name: v.value.name,
email: v.value.email,
message: v.value.body,
createdAt: Date.now(),
})
if (ok) await markContactNotified(db, id)
éèŠãªã®ã¯ãDiscord éç¥ã倱æããŠããDB ãžã®ä¿åã¯å¿
ãæåãããããšããé åºã§ããéç¥ã®å€±æã¯ notified = 0 ã®ãŸãŸæŸçœ®ãããç¿æ¥ 0 æïŒUTCïŒã«å®è¡ããã Cron ãæŸãçŽããŸãã
// vercel.json
{
"crons": [
{
"path": "/api/cron/notify-contacts",
"schedule": "0 0 * * *"
}
]
}
// src/app/api/cron/notify-contacts/route.tsïŒæç²ïŒ
// èªå·±ä¿®åŸ©: çä¿¡æã® Discord éç¥ãèœã¡ãåãåããïŒnotified=0ïŒãæ¯æ¥ååããã
const pending = await listPendingContacts(db, BATCH)
for (const c of pending) {
const ok = await notifyContact({ ... })
if (ok) {
await markContactNotified(db, c.id)
sent++
}
}
ãéç¥ã¯å°ãªããšãç¿æ¥ãŸã§ã«ã¯å±ãããšããä¿èšŒããå€éšãµãŒãã¹ãªãã»Turso ã ãã§å®çŸããŠããŸãããã®èªå·±ä¿®åŸ©ãã¿ãŒã³ã¯å¥ãããžã§ã¯ãã§ã暪å±éããŠãããã®ã§ãã
IP ããã·ã¥ã«ãããã©ã€ãã·ãŒä¿è·
ã¯ãªãã¯æ°ã䜿çšå ±åãèšé²ãããšããIPã¢ãã¬ã¹ããã®ãŸãŸä¿åããããšã¯å人æ å ±ã®èгç¹ããé¿ãããã§ãã
// src/lib/ip.ts
export function hashIp(ip: string, salt: string): string {
if (!ip) return ''
return createHash('sha256').update(`${salt}:${ip}`).digest('hex')
}
AUTH_SECRET ã salt ã«ã㊠SHA-256 ã§ããã·ã¥åããŸããå
ã® IP ã¢ãã¬ã¹ã¯ DB ã«æ®ãããåäž IP ããã®éè€æäœã ããæ€åºã§ããŸããIP ããã·ã¥+æéãŠã£ã³ããŠã§éè€ã¯ãªãã¯ãé€å€ããä»çµã¿ãåæ§ã§ãã
// src/lib/scoring.ts
export const CLICK_DEDUP_WINDOW_MS = 6 * 60 * 60 * 1000 // 6æé
6æé以å ã®åäž IP ããã·ã¥ããã®ã¯ãªãã¯ã¯1ä»¶ãšããŠã«ãŠã³ãããŸãã
èšèšã§è¿·ã£ãç¹ïŒèœãšã穎ïŒ
ãå ¬åŒç¹å žã®éé¡è¡šç€ºããšãInviteLoop ç¬èªã®å ±é ¬ãã®æ··å
perkJa / perkEn ã«ã玹ä»ã§âåããããŸãããšããå
Œξ
å ±ãæžããšããŠãŒã¶ãŒããInviteLoop ããéããããããšèª€è§£ããã±ãŒã¹ãæ³å®ãããŸããããã³ããšã³ãã®è¡šç€ºãèšèšãããšããããã®ãµãŒãã¹ã®å
¬åŒç¹å
žãã§ããããšãã©ãã«ã§æç¢ºã«åããããšã«ãªããŸãããDBã®ã«ã©ã åã ãã§ãªããUIã®æèšèšèšãŸã§é£åããŸãã
status ã® enum èšèš
åœå 'active' ãš 'hidden' ã ãã§ååããšæã£ãŠããŸãããã管çè
ãæå³çã«æ°žä¹
åé€ãããã±ãŒã¹ïŒæããã«è©æ¬ºçãªã³ãŒããªã©ïŒãçºçãããšãã®ããã« 'removed' ã远å ããŸãããæåãã3段éã«ããŠãããŠè¯ãã£ãã§ãã'hidden' ã¯ãŠãŒã¶ãŒéå ±ã§èªåé·ç§»ãã'removed' ã¯ç®¡çè
ãæç€ºçã«æäœãããšãã䜿ãåãã§ãã
Vercel Cron ã®æå°éé
Vercel ã® Hobby ãã©ã³ã§ã¯ Cron ã®æå°ééã1æ¥ã§ããããªã¢ã«ã¿ã€ã ã«è¿ãéç¥ã®ååãã¯ã§ããªãããããããŸã§ãã©ãŒã«ããã¯ãšå²ãåãèšèšã«ããŠããŸããæ¬çªã®éç¥ã¯çä¿¡ãšåæã«å±ããã¹ããšãã©ãŒãã§ãCron ã¯ä¿éºã§ãã
èªå·±æç¥šã»èªå·±äœ¿çšå ±åã®é²æ¢
vote ãš recordUsage ã®äž¡é¢æ°ã§ãèªåã®ã³ãŒããžã®æäœã匟ããŠããŸãã
// src/repos/engagement.tsïŒvote 颿°ã®æç²ïŒ
if (owner === userId) return { ok: false as const, reason: 'self' as const }
ãã®å¶çŽãå ¥ãå¿ãããšãæçš¿è ãèªåã®ã³ãŒãã®ã¹ã³ã¢ãäžãæŸé¡ã«ãªããŸãããå ±é ¬ã¯å ¬åŒç¹å žã ãããšããæ¹éã§ããã©ã³ãã³ã°äžäœãçãåæ©ã¯æ®ããããã¹ã³ã¢æäœã®é²ç·ã¯å¥éå¿ èŠã§ãã
ãŸãšã
- çŸéå ±é ¬ãæé€ããçç±ã¯èŠçŽãªã¹ã¯ã»ã¹ãã èªçºã»ã³ãã¥ããã£ã®å¥å šæ§ã®3ç¹
- æé€ã®å®è£ ã¯ã¹ããŒãã«å ±é ¬ã«ã©ã ãååšãããªãããšãæåã®äžæ
- äžæ£ææ¢ã¯
MAX_POSTS_PER_DAY = 5ã»1ãµãŒãã¹1ã³ãŒãã»URL ãã¡ã€ã³æ€èšŒã»3ä»¶ãªãŒããã€ãã®çµã¿åãã - å ¬å¹³ãªäžŠã³é 㯠clickÃ1ãusageÃ5ãupvoteÃ3 ã®ã¹ã³ã¢ãšå é確çã·ã£ããã«ã§å®çŸ
- ãåãåããéç¥ã¯ãDBä¿åâDiscordéç¥â倱æåã¯Cronãç¿æ¥ååãã®èªå·±ä¿®åŸ©ãã¿ãŒã³
- IPã¢ãã¬ã¹ã¯ SHA-256+salt ã§ããã·ã¥åããå ã®å€ã¯DBã«æ®ããªã
ãçŸéãªãããšããå¶çŽã¯æ©èœãåãããã«èŠããŠãå®éã«ã¯ãäœãé²ããªããã°ãããªããããæç¢ºã«ãªããããèšèšãã·ã³ãã«ã«ãªããŸããæåŸ ã³ãŒããšããæ§è³ªäžãã¹ãã ãšèŠçŽãªã¹ã¯ãæåã«å°ããŠããããšããé·ãéçšã§ãããµãŒãã¹ã®æ¡ä»¶ã ãšå€æããŠããŸãã
LilyïŒ@bokuwalilyïŒâ å人éçºè ãClaude Code ã§èªåååºç€ãçµã¿ãªãããiOSã¢ããªãWebãµãŒãã¹ãéç£ããŠããŸã
- äœã£ãã¢ããªã¯ ããŒããã©ãªãª ã«ãŸãšããŠããŸãð±
- æ°çã»éçºã®è£åŽã¯ X @bokuwalily ã§çºä¿¡ããŠããŸãð
- OSS: github.com/bokuwalily ð
çããã® â€ïž ãã·ã§ã¢ãå±ã¿ã«ãªããŸãïŒ