mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 16:29:40 +02:00
Merge branch 'dev' of https://github.com/Memo-2023/manacore-monorepo into dev
This commit is contained in:
commit
241dc6173e
89 changed files with 5246 additions and 3492 deletions
|
|
@ -1,662 +0,0 @@
|
|||
[
|
||||
{
|
||||
"timestamp": 1764952212026,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34261319680,
|
||||
"memoryFree": 98418688,
|
||||
"memoryUsagePercent": 99.71356391906738,
|
||||
"memoryEfficiency": 0.2864360809326172,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.18159993489583334,
|
||||
"platform": "darwin",
|
||||
"uptime": 316234
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952242025,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33978384384,
|
||||
"memoryFree": 381353984,
|
||||
"memoryUsagePercent": 98.8901138305664,
|
||||
"memoryEfficiency": 1.1098861694335938,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.3077799479166667,
|
||||
"platform": "darwin",
|
||||
"uptime": 316264
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952272027,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 31443189760,
|
||||
"memoryFree": 2916548608,
|
||||
"memoryUsagePercent": 91.51172637939453,
|
||||
"memoryEfficiency": 8.488273620605469,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.3214518229166667,
|
||||
"platform": "darwin",
|
||||
"uptime": 316294
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952302028,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 32673497088,
|
||||
"memoryFree": 1686241280,
|
||||
"memoryUsagePercent": 95.09239196777344,
|
||||
"memoryEfficiency": 4.9076080322265625,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.2928873697916667,
|
||||
"platform": "darwin",
|
||||
"uptime": 316324
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952332029,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33272102912,
|
||||
"memoryFree": 1087635456,
|
||||
"memoryUsagePercent": 96.83456420898438,
|
||||
"memoryEfficiency": 3.165435791015625,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.2457275390625,
|
||||
"platform": "darwin",
|
||||
"uptime": 316354
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952362030,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33491288064,
|
||||
"memoryFree": 868450304,
|
||||
"memoryUsagePercent": 97.47247695922852,
|
||||
"memoryEfficiency": 2.5275230407714844,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.21903483072916666,
|
||||
"platform": "darwin",
|
||||
"uptime": 316384
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952392031,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33697579008,
|
||||
"memoryFree": 662159360,
|
||||
"memoryUsagePercent": 98.07286262512207,
|
||||
"memoryEfficiency": 1.9271373748779297,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.19921875,
|
||||
"platform": "darwin",
|
||||
"uptime": 316414
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952422033,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33743257600,
|
||||
"memoryFree": 616480768,
|
||||
"memoryUsagePercent": 98.2058048248291,
|
||||
"memoryEfficiency": 1.7941951751708984,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.3559977213541667,
|
||||
"platform": "darwin",
|
||||
"uptime": 316444
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952454157,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34170847232,
|
||||
"memoryFree": 188891136,
|
||||
"memoryUsagePercent": 99.45025444030762,
|
||||
"memoryEfficiency": 0.5497455596923828,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.8998616536458334,
|
||||
"platform": "darwin",
|
||||
"uptime": 316476
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952484155,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34279800832,
|
||||
"memoryFree": 79937536,
|
||||
"memoryUsagePercent": 99.7673511505127,
|
||||
"memoryEfficiency": 0.2326488494873047,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.6012369791666666,
|
||||
"platform": "darwin",
|
||||
"uptime": 316506
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952516186,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34296479744,
|
||||
"memoryFree": 63258624,
|
||||
"memoryUsagePercent": 99.81589317321777,
|
||||
"memoryEfficiency": 0.18410682678222656,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.4691975911458333,
|
||||
"platform": "darwin",
|
||||
"uptime": 316538
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952548258,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34266447872,
|
||||
"memoryFree": 93290496,
|
||||
"memoryUsagePercent": 99.72848892211914,
|
||||
"memoryEfficiency": 0.2715110778808594,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.340087890625,
|
||||
"platform": "darwin",
|
||||
"uptime": 316570
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952578260,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34260549632,
|
||||
"memoryFree": 99188736,
|
||||
"memoryUsagePercent": 99.71132278442383,
|
||||
"memoryEfficiency": 0.2886772155761719,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.2496337890625,
|
||||
"platform": "darwin",
|
||||
"uptime": 316600
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952609578,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33692418048,
|
||||
"memoryFree": 667320320,
|
||||
"memoryUsagePercent": 98.05784225463867,
|
||||
"memoryEfficiency": 1.9421577453613281,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.19681803385416666,
|
||||
"platform": "darwin",
|
||||
"uptime": 316631
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952641004,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34198962176,
|
||||
"memoryFree": 160776192,
|
||||
"memoryUsagePercent": 99.53207969665527,
|
||||
"memoryEfficiency": 0.46792030334472656,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.23486328125,
|
||||
"platform": "darwin",
|
||||
"uptime": 316663
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952671007,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34222686208,
|
||||
"memoryFree": 137052160,
|
||||
"memoryUsagePercent": 99.60112571716309,
|
||||
"memoryEfficiency": 0.39887428283691406,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.18770345052083334,
|
||||
"platform": "darwin",
|
||||
"uptime": 316693
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952702795,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34269872128,
|
||||
"memoryFree": 89866240,
|
||||
"memoryUsagePercent": 99.73845481872559,
|
||||
"memoryEfficiency": 0.26154518127441406,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.15791829427083334,
|
||||
"platform": "darwin",
|
||||
"uptime": 316724
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952732797,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34242723840,
|
||||
"memoryFree": 117014528,
|
||||
"memoryUsagePercent": 99.65944290161133,
|
||||
"memoryEfficiency": 0.3405570983886719,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.12320963541666667,
|
||||
"platform": "darwin",
|
||||
"uptime": 316754
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952764879,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34213707776,
|
||||
"memoryFree": 146030592,
|
||||
"memoryUsagePercent": 99.57499504089355,
|
||||
"memoryEfficiency": 0.4250049591064453,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.11909993489583333,
|
||||
"platform": "darwin",
|
||||
"uptime": 316786
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952796332,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34039250944,
|
||||
"memoryFree": 320487424,
|
||||
"memoryUsagePercent": 99.06725883483887,
|
||||
"memoryEfficiency": 0.9327411651611328,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.135009765625,
|
||||
"platform": "darwin",
|
||||
"uptime": 316818
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952826334,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34186772480,
|
||||
"memoryFree": 172965888,
|
||||
"memoryUsagePercent": 99.49660301208496,
|
||||
"memoryEfficiency": 0.5033969879150391,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.21162923177083334,
|
||||
"platform": "darwin",
|
||||
"uptime": 316848
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952858356,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34186379264,
|
||||
"memoryFree": 173359104,
|
||||
"memoryUsagePercent": 99.49545860290527,
|
||||
"memoryEfficiency": 0.5045413970947266,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.2408447265625,
|
||||
"platform": "darwin",
|
||||
"uptime": 316880
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952889730,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34280357888,
|
||||
"memoryFree": 79380480,
|
||||
"memoryUsagePercent": 99.76897239685059,
|
||||
"memoryEfficiency": 0.23102760314941406,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.3212890625,
|
||||
"platform": "darwin",
|
||||
"uptime": 316911
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952919730,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33718976512,
|
||||
"memoryFree": 640761856,
|
||||
"memoryUsagePercent": 98.1351375579834,
|
||||
"memoryEfficiency": 1.8648624420166016,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.2577311197916667,
|
||||
"platform": "darwin",
|
||||
"uptime": 316941
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952949731,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33727643648,
|
||||
"memoryFree": 632094720,
|
||||
"memoryUsagePercent": 98.16036224365234,
|
||||
"memoryEfficiency": 1.8396377563476562,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.2508138020833333,
|
||||
"platform": "darwin",
|
||||
"uptime": 316971
|
||||
},
|
||||
{
|
||||
"timestamp": 1764952979732,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33519927296,
|
||||
"memoryFree": 839811072,
|
||||
"memoryUsagePercent": 97.55582809448242,
|
||||
"memoryEfficiency": 2.444171905517578,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 1.6138916015625,
|
||||
"platform": "darwin",
|
||||
"uptime": 317001
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953009732,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33729118208,
|
||||
"memoryFree": 630620160,
|
||||
"memoryUsagePercent": 98.16465377807617,
|
||||
"memoryEfficiency": 1.8353462219238281,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 1.1908365885416667,
|
||||
"platform": "darwin",
|
||||
"uptime": 317031
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953039733,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34194341888,
|
||||
"memoryFree": 165396480,
|
||||
"memoryUsagePercent": 99.51863288879395,
|
||||
"memoryEfficiency": 0.4813671112060547,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.8716227213541666,
|
||||
"platform": "darwin",
|
||||
"uptime": 317061
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953069734,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34195587072,
|
||||
"memoryFree": 164151296,
|
||||
"memoryUsagePercent": 99.52225685119629,
|
||||
"memoryEfficiency": 0.47774314880371094,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.6322835286458334,
|
||||
"platform": "darwin",
|
||||
"uptime": 317091
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953099734,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34240970752,
|
||||
"memoryFree": 118767616,
|
||||
"memoryUsagePercent": 99.65434074401855,
|
||||
"memoryEfficiency": 0.3456592559814453,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.5132242838541666,
|
||||
"platform": "darwin",
|
||||
"uptime": 317121
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953129734,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34246590464,
|
||||
"memoryFree": 113147904,
|
||||
"memoryUsagePercent": 99.67069625854492,
|
||||
"memoryEfficiency": 0.3293037414550781,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.4134521484375,
|
||||
"platform": "darwin",
|
||||
"uptime": 317151
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953159735,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34252914688,
|
||||
"memoryFree": 106823680,
|
||||
"memoryUsagePercent": 99.68910217285156,
|
||||
"memoryEfficiency": 0.3108978271484375,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.5005696614583334,
|
||||
"platform": "darwin",
|
||||
"uptime": 317181
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953189735,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34234728448,
|
||||
"memoryFree": 125009920,
|
||||
"memoryUsagePercent": 99.63617324829102,
|
||||
"memoryEfficiency": 0.3638267517089844,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.3972981770833333,
|
||||
"platform": "darwin",
|
||||
"uptime": 317211
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953219736,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33779810304,
|
||||
"memoryFree": 579928064,
|
||||
"memoryUsagePercent": 98.31218719482422,
|
||||
"memoryEfficiency": 1.6878128051757812,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.3188883463541667,
|
||||
"platform": "darwin",
|
||||
"uptime": 317241
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953249736,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34159230976,
|
||||
"memoryFree": 200507392,
|
||||
"memoryUsagePercent": 99.41644668579102,
|
||||
"memoryEfficiency": 0.5835533142089844,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.2990315755208333,
|
||||
"platform": "darwin",
|
||||
"uptime": 317271
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953279736,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34207203328,
|
||||
"memoryFree": 152535040,
|
||||
"memoryUsagePercent": 99.55606460571289,
|
||||
"memoryEfficiency": 0.4439353942871094,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.4497477213541667,
|
||||
"platform": "darwin",
|
||||
"uptime": 317301
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953309737,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34149695488,
|
||||
"memoryFree": 210042880,
|
||||
"memoryUsagePercent": 99.3886947631836,
|
||||
"memoryEfficiency": 0.6113052368164062,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.3463541666666667,
|
||||
"platform": "darwin",
|
||||
"uptime": 317331
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953339738,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33949761536,
|
||||
"memoryFree": 409976832,
|
||||
"memoryUsagePercent": 98.80681037902832,
|
||||
"memoryEfficiency": 1.1931896209716797,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.3301188151041667,
|
||||
"platform": "darwin",
|
||||
"uptime": 317361
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953369738,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34282471424,
|
||||
"memoryFree": 77266944,
|
||||
"memoryUsagePercent": 99.7751235961914,
|
||||
"memoryEfficiency": 0.22487640380859375,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.28466796875,
|
||||
"platform": "darwin",
|
||||
"uptime": 317391
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953399740,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 32868974592,
|
||||
"memoryFree": 1490763776,
|
||||
"memoryUsagePercent": 95.66130638122559,
|
||||
"memoryEfficiency": 4.338693618774414,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.224365234375,
|
||||
"platform": "darwin",
|
||||
"uptime": 317421
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953429741,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33954299904,
|
||||
"memoryFree": 405438464,
|
||||
"memoryUsagePercent": 98.82001876831055,
|
||||
"memoryEfficiency": 1.1799812316894531,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.18465169270833334,
|
||||
"platform": "darwin",
|
||||
"uptime": 317451
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953459742,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33574420480,
|
||||
"memoryFree": 785317888,
|
||||
"memoryUsagePercent": 97.71442413330078,
|
||||
"memoryEfficiency": 2.2855758666992188,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.1524658203125,
|
||||
"platform": "darwin",
|
||||
"uptime": 317481
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953489742,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33800306688,
|
||||
"memoryFree": 559431680,
|
||||
"memoryUsagePercent": 98.37183952331543,
|
||||
"memoryEfficiency": 1.6281604766845703,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.20756022135416666,
|
||||
"platform": "darwin",
|
||||
"uptime": 317511
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953519743,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34025553920,
|
||||
"memoryFree": 334184448,
|
||||
"memoryUsagePercent": 99.02739524841309,
|
||||
"memoryEfficiency": 0.9726047515869141,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.23970540364583334,
|
||||
"platform": "darwin",
|
||||
"uptime": 317541
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953549744,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34073149440,
|
||||
"memoryFree": 286588928,
|
||||
"memoryUsagePercent": 99.1659164428711,
|
||||
"memoryEfficiency": 0.8340835571289062,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.2720133463541667,
|
||||
"platform": "darwin",
|
||||
"uptime": 317571
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953579745,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34250440704,
|
||||
"memoryFree": 109297664,
|
||||
"memoryUsagePercent": 99.6819019317627,
|
||||
"memoryEfficiency": 0.3180980682373047,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.2032470703125,
|
||||
"platform": "darwin",
|
||||
"uptime": 317601
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953609745,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34292432896,
|
||||
"memoryFree": 67305472,
|
||||
"memoryUsagePercent": 99.80411529541016,
|
||||
"memoryEfficiency": 0.19588470458984375,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.22269694010416666,
|
||||
"platform": "darwin",
|
||||
"uptime": 317631
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953639747,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34245033984,
|
||||
"memoryFree": 114704384,
|
||||
"memoryUsagePercent": 99.66616630554199,
|
||||
"memoryEfficiency": 0.3338336944580078,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.1802978515625,
|
||||
"platform": "darwin",
|
||||
"uptime": 317661
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953669748,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34262171648,
|
||||
"memoryFree": 97566720,
|
||||
"memoryUsagePercent": 99.71604347229004,
|
||||
"memoryEfficiency": 0.28395652770996094,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.16731770833333334,
|
||||
"platform": "darwin",
|
||||
"uptime": 317691
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953699748,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33642708992,
|
||||
"memoryFree": 717029376,
|
||||
"memoryUsagePercent": 97.91316986083984,
|
||||
"memoryEfficiency": 2.0868301391601562,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.1925048828125,
|
||||
"platform": "darwin",
|
||||
"uptime": 317721
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953729749,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 33480491008,
|
||||
"memoryFree": 879247360,
|
||||
"memoryUsagePercent": 97.44105339050293,
|
||||
"memoryEfficiency": 2.5589466094970703,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.327880859375,
|
||||
"platform": "darwin",
|
||||
"uptime": 317751
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953759749,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34072002560,
|
||||
"memoryFree": 287735808,
|
||||
"memoryUsagePercent": 99.16257858276367,
|
||||
"memoryEfficiency": 0.8374214172363281,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.2535400390625,
|
||||
"platform": "darwin",
|
||||
"uptime": 317781
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953789750,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34169667584,
|
||||
"memoryFree": 190070784,
|
||||
"memoryUsagePercent": 99.44682121276855,
|
||||
"memoryEfficiency": 0.5531787872314453,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.19832356770833334,
|
||||
"platform": "darwin",
|
||||
"uptime": 317811
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953819750,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34144272384,
|
||||
"memoryFree": 215465984,
|
||||
"memoryUsagePercent": 99.37291145324707,
|
||||
"memoryEfficiency": 0.6270885467529297,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.209716796875,
|
||||
"platform": "darwin",
|
||||
"uptime": 317841
|
||||
},
|
||||
{
|
||||
"timestamp": 1764953849751,
|
||||
"memoryTotal": 34359738368,
|
||||
"memoryUsed": 34237202432,
|
||||
"memoryFree": 122535936,
|
||||
"memoryUsagePercent": 99.64337348937988,
|
||||
"memoryEfficiency": 0.3566265106201172,
|
||||
"cpuCount": 12,
|
||||
"cpuLoad": 0.24357096354166666,
|
||||
"platform": "darwin",
|
||||
"uptime": 317871
|
||||
}
|
||||
]
|
||||
|
|
@ -349,6 +349,15 @@ async function getPaginated(
|
|||
|
||||
## Migrations
|
||||
|
||||
> **Comprehensive Documentation**: See **[docs/DATABASE_MIGRATIONS.md](/docs/DATABASE_MIGRATIONS.md)** for full migration internals, CI/CD integration, zero-downtime patterns, and troubleshooting.
|
||||
|
||||
### Quick Reference
|
||||
|
||||
| Environment | Command | Purpose |
|
||||
| --------------- | ----------------- | ------------------------------- |
|
||||
| **Development** | `pnpm db:push` | Fast iteration, direct sync |
|
||||
| **Production** | `pnpm db:migrate` | Tracked migrations with history |
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
|
|
@ -358,9 +367,9 @@ import { defineConfig } from 'drizzle-kit';
|
|||
export default defineConfig({
|
||||
schema: './src/db/schema/index.ts',
|
||||
out: './src/db/migrations',
|
||||
driver: 'pg',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
connectionString: process.env.DATABASE_URL!,
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
verbose: true,
|
||||
strict: true,
|
||||
|
|
@ -370,41 +379,85 @@ export default defineConfig({
|
|||
### Commands
|
||||
|
||||
```bash
|
||||
# Generate migration from schema changes
|
||||
pnpm drizzle-kit generate
|
||||
# Development - push schema directly (fast, no history)
|
||||
pnpm db:push
|
||||
|
||||
# Push schema directly (development only)
|
||||
pnpm drizzle-kit push
|
||||
|
||||
# Open Drizzle Studio
|
||||
pnpm drizzle-kit studio
|
||||
|
||||
# Run migrations (production)
|
||||
# Production - generate and run migrations
|
||||
pnpm db:generate --name add_user_preferences
|
||||
pnpm db:migrate
|
||||
|
||||
# Open Drizzle Studio for database inspection
|
||||
pnpm db:studio
|
||||
```
|
||||
|
||||
### Migration Runner
|
||||
### Migration Workflow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Which command should I use? │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Local development? │
|
||||
│ └── YES → pnpm db:push (fast, no tracking) │
|
||||
│ │
|
||||
│ Staging/Production? │
|
||||
│ └── YES → pnpm db:generate + pnpm db:migrate (tracked) │
|
||||
│ │
|
||||
│ Schema changed by someone else? │
|
||||
│ └── YES → git pull + pnpm db:push (local) │
|
||||
│ git pull + pnpm db:migrate (staging/prod) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Key Concepts
|
||||
|
||||
1. **Advisory Locks**: Migrations use PostgreSQL advisory locks to prevent concurrent execution
|
||||
2. **Migration Tracking**: `__drizzle_migrations` table + `meta/_journal.json` file
|
||||
3. **Migrations run BEFORE code deployment**: Ensures database is ready for new code
|
||||
4. **Never modify applied migrations**: Create new migrations instead
|
||||
5. **Zero-downtime**: Use expand-contract pattern for breaking schema changes
|
||||
|
||||
### Production Migration Script
|
||||
|
||||
Production backends use a migration script with advisory locks:
|
||||
|
||||
```typescript
|
||||
// src/db/migrate.ts
|
||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import { migrate } from 'drizzle-orm/postgres-js/migrator';
|
||||
import postgres from 'postgres';
|
||||
// src/db/migrate.ts - Key features:
|
||||
// - Advisory lock (pg_try_advisory_lock) prevents concurrent migrations
|
||||
// - Retry logic with exponential backoff for transient failures
|
||||
// - Timeout protection (default 5 minutes)
|
||||
// - Graceful handling when no migrations exist
|
||||
|
||||
async function runMigrations() {
|
||||
const connection = postgres(process.env.DATABASE_URL!, { max: 1 });
|
||||
const db = drizzle(connection);
|
||||
const MIGRATION_LOCK_ID = 987654321; // Unique per service
|
||||
|
||||
console.log('Running migrations...');
|
||||
await migrate(db, { migrationsFolder: './src/db/migrations' });
|
||||
console.log('Migrations complete');
|
||||
|
||||
await connection.end();
|
||||
async function acquireLock(db) {
|
||||
const result = await db.execute(
|
||||
sql`SELECT pg_try_advisory_lock(${MIGRATION_LOCK_ID}) as acquired`
|
||||
);
|
||||
return result[0]?.acquired === true;
|
||||
}
|
||||
|
||||
runMigrations().catch(console.error);
|
||||
```
|
||||
|
||||
See `services/mana-core-auth/src/db/migrate.ts` for the full implementation.
|
||||
|
||||
### Best Practices
|
||||
|
||||
**DO:**
|
||||
|
||||
- Run migrations before deploying new code
|
||||
- Test migrations in staging before production
|
||||
- Use `CONCURRENTLY` for index creation
|
||||
- Keep migrations small and focused
|
||||
- Commit migration files to version control
|
||||
|
||||
**DON'T:**
|
||||
|
||||
- Run `db:push` in production
|
||||
- Delete or modify applied migrations
|
||||
- Add NOT NULL without default or backfill
|
||||
- Drop columns immediately (wait 1-2 weeks)
|
||||
|
||||
## Query Patterns
|
||||
|
||||
### Select with Joins
|
||||
|
|
|
|||
|
|
@ -698,15 +698,138 @@ export default {
|
|||
|
||||
## Environment Variables
|
||||
|
||||
```typescript
|
||||
// Access in .svelte or .ts files
|
||||
import { PUBLIC_BACKEND_URL, PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public';
|
||||
### Build-Time vs Runtime Variables
|
||||
|
||||
// .env file
|
||||
SvelteKit has **two types** of environment variables:
|
||||
|
||||
1. **Build-time** (`$env/static/public`) - Baked into the bundle at build time
|
||||
2. **Runtime** (`process.env`) - Available at runtime in server code
|
||||
|
||||
**CRITICAL**: For Docker deployments, browser-facing URLs must use **runtime injection** because:
|
||||
|
||||
- Docker images are built once but deployed to different environments (staging, production)
|
||||
- Build-time variables would require rebuilding the image for each environment
|
||||
- The browser cannot access `process.env` - it needs values injected into the HTML
|
||||
|
||||
### ❌ WRONG - Hardcoded or Build-Time URLs
|
||||
|
||||
```typescript
|
||||
// ❌ BAD - Hardcoded URL (won't work in Docker)
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
|
||||
// ❌ BAD - Build-time variable (works locally, breaks in Docker)
|
||||
import { PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public';
|
||||
const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
|
||||
// ❌ BAD - import.meta.env is also build-time
|
||||
const MANA_AUTH_URL = import.meta.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
```
|
||||
|
||||
### ✅ CORRECT - Runtime Injection Pattern
|
||||
|
||||
**Step 1: Create `hooks.server.ts`** to inject env vars into HTML:
|
||||
|
||||
```typescript
|
||||
// src/hooks.server.ts
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
// Get client-side URLs from Docker runtime environment
|
||||
const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
|
||||
process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || '';
|
||||
const PUBLIC_BACKEND_URL_CLIENT =
|
||||
process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || '';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) => {
|
||||
// Inject runtime environment variables into the HTML
|
||||
const envScript = `<script>
|
||||
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${PUBLIC_MANA_CORE_AUTH_URL_CLIENT}";
|
||||
window.__PUBLIC_BACKEND_URL__ = "${PUBLIC_BACKEND_URL_CLIENT}";
|
||||
</script>`;
|
||||
return html.replace('<head>', `<head>${envScript}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
**Step 2: Read from `window` in client code:**
|
||||
|
||||
```typescript
|
||||
// src/lib/stores/auth.svelte.ts
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
// Client-side: use injected window variable
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
// Server-side (SSR): use Docker internal URL
|
||||
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Use in auth service initialization
|
||||
const auth = initializeWebAuth({ baseUrl: getAuthUrl() });
|
||||
```
|
||||
|
||||
**Step 3: Set environment variables in `docker-compose.staging.yml`:**
|
||||
|
||||
```yaml
|
||||
services:
|
||||
myapp-web:
|
||||
environment:
|
||||
# Server-side URLs (Docker internal network)
|
||||
PUBLIC_BACKEND_URL: http://myapp-backend:3000
|
||||
PUBLIC_MANA_CORE_AUTH_URL: http://mana-core-auth:3001
|
||||
# Client-side URLs (browser access via public IP)
|
||||
PUBLIC_BACKEND_URL_CLIENT: http://46.224.108.214:3000
|
||||
PUBLIC_MANA_CORE_AUTH_URL_CLIENT: http://46.224.108.214:3001
|
||||
```
|
||||
|
||||
### Why Two URLs?
|
||||
|
||||
| Variable | Purpose | Example |
|
||||
| ---------------------------------- | --------------------------------- | ---------------------------- |
|
||||
| `PUBLIC_MANA_CORE_AUTH_URL` | Server-to-server (SSR, API calls) | `http://mana-core-auth:3001` |
|
||||
| `PUBLIC_MANA_CORE_AUTH_URL_CLIENT` | Browser to server | `http://46.224.108.214:3001` |
|
||||
|
||||
Docker containers can reach each other by service name (`mana-core-auth`), but browsers need the public IP/domain.
|
||||
|
||||
### Apps Using This Pattern Correctly
|
||||
|
||||
- ✅ `chat/apps/web` - Has `hooks.server.ts` with runtime injection
|
||||
- ✅ `todo/apps/web` - Fixed
|
||||
- ✅ `calendar/apps/web` - Fixed
|
||||
- ✅ `clock/apps/web` - Fixed
|
||||
|
||||
### Apps That Still Need Fixing
|
||||
|
||||
- ❌ `contacts/apps/web`
|
||||
- ❌ `manadeck/apps/web`
|
||||
- ❌ `manacore/apps/web`
|
||||
- ❌ `zitare/apps/web`
|
||||
- ❌ `picture/apps/web`
|
||||
|
||||
### Quick Checklist for New SvelteKit Apps
|
||||
|
||||
- [ ] Create `src/hooks.server.ts` with env injection
|
||||
- [ ] Update `auth.svelte.ts` to use `getAuthUrl()` pattern
|
||||
- [ ] Update `user-settings.svelte.ts` to use `getAuthUrl()` pattern
|
||||
- [ ] Update any feedback services to use runtime URL
|
||||
- [ ] Add both `_CLIENT` and non-client env vars to `docker-compose.staging.yml`
|
||||
- [ ] Never hardcode `localhost:3001` anywhere
|
||||
|
||||
### Simple .env (for local development only)
|
||||
|
||||
```env
|
||||
PUBLIC_BACKEND_URL=http://localhost:3016
|
||||
PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
```
|
||||
|
||||
These work locally because both the browser and server access `localhost`.
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### Don't Use Old Svelte Syntax
|
||||
|
|
|
|||
55
.github/workflows/cd-production.yml
vendored
55
.github/workflows/cd-production.yml
vendored
|
|
@ -1,3 +1,10 @@
|
|||
# Production Deployment
|
||||
#
|
||||
# Triggered by:
|
||||
# - Manual only (workflow_dispatch with confirmation)
|
||||
#
|
||||
# Flow: dev (staging) → main (production)
|
||||
# Requires typing "deploy" to confirm
|
||||
name: CD - Production Deployment
|
||||
|
||||
on:
|
||||
|
|
@ -205,8 +212,52 @@ jobs:
|
|||
ssh ${{ secrets.PRODUCTION_USER }}@${{ secrets.PRODUCTION_HOST }} << 'EOF'
|
||||
cd ~/manacore-production
|
||||
|
||||
# Run migrations before deploying new code
|
||||
docker compose run --rm mana-core-auth pnpm run db:migrate || echo "Migrations completed or skipped"
|
||||
echo "=== Running Database Migrations ==="
|
||||
echo ""
|
||||
|
||||
# Migration function with retry logic
|
||||
run_migration() {
|
||||
local service=$1
|
||||
local max_attempts=3
|
||||
local timeout=300 # 5 minutes
|
||||
local attempt=1
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
echo "[$service] Migration attempt $attempt/$max_attempts..."
|
||||
|
||||
# Run migration with timeout using a temporary container
|
||||
if timeout $timeout docker compose run --rm $service pnpm run db:migrate 2>&1; then
|
||||
echo "✅ [$service] Migration succeeded"
|
||||
return 0
|
||||
else
|
||||
exit_code=$?
|
||||
if [ $exit_code -eq 124 ]; then
|
||||
echo "⚠️ [$service] Migration timeout after ${timeout}s"
|
||||
else
|
||||
echo "⚠️ [$service] Migration failed with exit code $exit_code"
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
if [ $attempt -le $max_attempts ]; then
|
||||
wait_time=$((10 * attempt)) # Backoff: 10s, 20s, 30s
|
||||
echo " Waiting ${wait_time}s before retry..."
|
||||
sleep $wait_time
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "❌ [$service] Migration failed after $max_attempts attempts"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Run migrations for mana-core-auth (central auth service)
|
||||
run_migration mana-core-auth || {
|
||||
echo "❌ mana-core-auth migration failed"
|
||||
echo "⚠️ Continuing with deployment - manual migration may be required"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "✅ Migration step completed"
|
||||
EOF
|
||||
|
||||
- name: Deploy with zero-downtime
|
||||
|
|
|
|||
102
.github/workflows/cd-staging-tagged.yml
vendored
102
.github/workflows/cd-staging-tagged.yml
vendored
|
|
@ -21,6 +21,10 @@ on:
|
|||
- zitare
|
||||
- presi
|
||||
- mana-core-auth
|
||||
- manacore
|
||||
- todo
|
||||
- calendar
|
||||
- clock
|
||||
apps:
|
||||
description: 'Apps to deploy (comma-separated: backend,web,landing or "all")'
|
||||
required: true
|
||||
|
|
@ -36,7 +40,8 @@ env:
|
|||
NODE_VERSION: '20'
|
||||
PNPM_VERSION: '9.15.0'
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_PREFIX: ghcr.io/${{ github.repository_owner }}
|
||||
# Note: repository_owner is lowercased for Docker compatibility
|
||||
IMAGE_PREFIX: ghcr.io/memo-2023
|
||||
|
||||
jobs:
|
||||
# Parse tag or inputs to determine what to deploy
|
||||
|
|
@ -104,6 +109,10 @@ jobs:
|
|||
PROJECT_APPS[zitare]="backend,web"
|
||||
PROJECT_APPS[presi]="backend,web"
|
||||
PROJECT_APPS[mana-core-auth]="service"
|
||||
PROJECT_APPS[manacore]="web"
|
||||
PROJECT_APPS[todo]="backend,web"
|
||||
PROJECT_APPS[calendar]="backend,web"
|
||||
PROJECT_APPS[clock]="backend,web"
|
||||
|
||||
# Expand "all" to available apps
|
||||
if [ "$APPS" == "all" ]; then
|
||||
|
|
@ -135,8 +144,17 @@ jobs:
|
|||
IMAGE_NAME="${PROJECT}-backend"
|
||||
;;
|
||||
web)
|
||||
DOCKERFILE_PATH="docker/templates/Dockerfile.sveltekit"
|
||||
CONTEXT_PATH="apps/$PROJECT/apps/web"
|
||||
# Apps with their own Dockerfiles (need monorepo root for shared packages)
|
||||
case "$PROJECT" in
|
||||
manacore|todo|calendar|clock)
|
||||
DOCKERFILE_PATH="apps/$PROJECT/apps/web/Dockerfile"
|
||||
CONTEXT_PATH="."
|
||||
;;
|
||||
*)
|
||||
DOCKERFILE_PATH="docker/templates/Dockerfile.sveltekit"
|
||||
CONTEXT_PATH="apps/$PROJECT/apps/web"
|
||||
;;
|
||||
esac
|
||||
IMAGE_NAME="${PROJECT}-web"
|
||||
;;
|
||||
landing)
|
||||
|
|
@ -146,15 +164,29 @@ jobs:
|
|||
;;
|
||||
esac
|
||||
|
||||
# Set ports per project
|
||||
# Set backend ports per project (must match docker-compose.staging.yml)
|
||||
case "$PROJECT" in
|
||||
chat) PORT="3002" ;;
|
||||
picture) PORT="3006" ;;
|
||||
manadeck) PORT="3009" ;;
|
||||
zitare) PORT="3007" ;;
|
||||
presi) PORT="3008" ;;
|
||||
calendar) PORT="3016" ;;
|
||||
clock) PORT="3017" ;;
|
||||
todo) PORT="3018" ;;
|
||||
esac
|
||||
HEALTH_PATH="/api/health"
|
||||
|
||||
# Override ports for web apps (SvelteKit uses different ports)
|
||||
if [ "$APP" == "web" ]; then
|
||||
case "$PROJECT" in
|
||||
manacore) PORT="5173" ;;
|
||||
todo) PORT="5188" ;;
|
||||
calendar) PORT="5186" ;;
|
||||
clock) PORT="5187" ;;
|
||||
*) PORT="5173" ;; # default SvelteKit port
|
||||
esac
|
||||
fi
|
||||
HEALTH_PATH="/api/v1/health"
|
||||
;;
|
||||
esac
|
||||
|
||||
|
|
@ -267,25 +299,71 @@ jobs:
|
|||
mkdir -p ~/.ssh
|
||||
ssh-keyscan -H ${{ secrets.STAGING_HOST }} >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Sync docker-compose to staging
|
||||
run: |
|
||||
# Ensure staging directory exists
|
||||
ssh ${{ secrets.STAGING_USER }}@${{ secrets.STAGING_HOST }} "mkdir -p ~/manacore-staging"
|
||||
# Copy the docker-compose file
|
||||
scp docker-compose.staging.yml ${{ secrets.STAGING_USER }}@${{ secrets.STAGING_HOST }}:~/manacore-staging/docker-compose.yml
|
||||
|
||||
- name: Login to GHCR on staging server
|
||||
run: |
|
||||
ssh ${{ secrets.STAGING_USER }}@${{ secrets.STAGING_HOST }} << EOF
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
EOF
|
||||
|
||||
- name: Deploy service
|
||||
env:
|
||||
VERSION: ${{ needs.parse-deployment.outputs.version }}
|
||||
IMAGE_NAME: ${{ matrix.image_name }}
|
||||
APP_TYPE: ${{ matrix.app }}
|
||||
PROJECT: ${{ needs.parse-deployment.outputs.project }}
|
||||
run: |
|
||||
# Compute the version variable name locally (before SSH)
|
||||
# Map: todo-web -> TODO_WEB_VERSION, chat-backend -> CHAT_VERSION
|
||||
case "$IMAGE_NAME" in
|
||||
*-web)
|
||||
PROJECT_UPPER=$(echo "$PROJECT" | tr '[:lower:]-' '[:upper:]_')
|
||||
VERSION_VAR="${PROJECT_UPPER}_WEB_VERSION"
|
||||
;;
|
||||
*-backend)
|
||||
PROJECT_UPPER=$(echo "$PROJECT" | tr '[:lower:]-' '[:upper:]_')
|
||||
VERSION_VAR="${PROJECT_UPPER}_VERSION"
|
||||
;;
|
||||
mana-core-auth)
|
||||
VERSION_VAR="AUTH_VERSION"
|
||||
;;
|
||||
*)
|
||||
VERSION_VAR=$(echo "$IMAGE_NAME" | tr '[:lower:]-' '[:upper:]_')_VERSION
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Will set $VERSION_VAR=$VERSION for docker-compose"
|
||||
|
||||
ssh ${{ secrets.STAGING_USER }}@${{ secrets.STAGING_HOST }} << EOF
|
||||
cd ~/manacore-staging
|
||||
|
||||
echo "Deploying $IMAGE_NAME:$VERSION to staging..."
|
||||
|
||||
# Pull the new image
|
||||
# Pull the new image with specific version tag
|
||||
docker pull ${{ env.IMAGE_PREFIX }}/$IMAGE_NAME:$VERSION
|
||||
|
||||
# Restart only the specific service
|
||||
SERVICE_NAME=\$(echo "$IMAGE_NAME" | tr '-' '_')
|
||||
# Update .env file with the version for this service
|
||||
# This ensures docker-compose uses the correct image tag
|
||||
if grep -q "^$VERSION_VAR=" .env 2>/dev/null; then
|
||||
sed -i "s/^$VERSION_VAR=.*/$VERSION_VAR=$VERSION/" .env
|
||||
else
|
||||
echo "$VERSION_VAR=$VERSION" >> .env
|
||||
fi
|
||||
|
||||
if docker compose ps | grep -q "\$SERVICE_NAME"; then
|
||||
echo "Updated .env: $VERSION_VAR=$VERSION"
|
||||
grep "$VERSION_VAR" .env || true
|
||||
|
||||
# Service name matches docker-compose service name (with hyphens)
|
||||
SERVICE_NAME="$IMAGE_NAME"
|
||||
|
||||
if docker compose ps -a | grep -q "$IMAGE_NAME"; then
|
||||
echo "Updating existing service: \$SERVICE_NAME"
|
||||
docker compose pull \$SERVICE_NAME || true
|
||||
docker compose up -d --no-deps --force-recreate \$SERVICE_NAME
|
||||
else
|
||||
echo "Service \$SERVICE_NAME not found in compose, starting..."
|
||||
|
|
@ -296,6 +374,10 @@ jobs:
|
|||
sleep 10
|
||||
docker compose ps \$SERVICE_NAME
|
||||
|
||||
# Verify correct image is running
|
||||
echo "Running image:"
|
||||
docker inspect --format='{{.Config.Image}}' ${IMAGE_NAME}-staging 2>/dev/null || true
|
||||
|
||||
# Cleanup old images
|
||||
docker image prune -f
|
||||
EOF
|
||||
|
|
|
|||
196
.github/workflows/cd-staging.yml
vendored
196
.github/workflows/cd-staging.yml
vendored
|
|
@ -1,11 +1,13 @@
|
|||
# Simplified staging config: mana-core-auth + chat-backend only
|
||||
# Staging Deployment
|
||||
#
|
||||
# Triggered by:
|
||||
# - Automatic: Push to dev branch (via ci.yml)
|
||||
# - Manual: workflow_dispatch
|
||||
#
|
||||
# Full config archived at: .github/workflows/cd-staging.full.yml
|
||||
#
|
||||
# To restore full config:
|
||||
# cp .github/workflows/cd-staging.full.yml .github/workflows/cd-staging.yml
|
||||
#
|
||||
# To add a service back:
|
||||
# 1. Add service to workflow_dispatch options (line ~10)
|
||||
# To add a service:
|
||||
# 1. Add service to workflow_dispatch options
|
||||
# 2. Add health check in "Run health checks" step
|
||||
# 3. Add service to docker-compose.staging.yml
|
||||
name: CD - Staging Deployment
|
||||
|
|
@ -22,6 +24,13 @@ on:
|
|||
- mana-core-auth
|
||||
- chat-backend
|
||||
- chat-web
|
||||
- manacore-web
|
||||
- todo-backend
|
||||
- todo-web
|
||||
- calendar-backend
|
||||
- calendar-web
|
||||
- clock-backend
|
||||
- clock-web
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
|
|
@ -182,9 +191,81 @@ jobs:
|
|||
# Create chat database (for chat-backend service)
|
||||
docker compose exec -T postgres psql -U postgres -c "CREATE DATABASE chat;" 2>/dev/null || echo "chat database already exists"
|
||||
|
||||
# Create todo database (for todo-backend service)
|
||||
docker compose exec -T postgres psql -U postgres -c "CREATE DATABASE todo;" 2>/dev/null || echo "todo database already exists"
|
||||
|
||||
# Create calendar database (for calendar-backend service)
|
||||
docker compose exec -T postgres psql -U postgres -c "CREATE DATABASE calendar;" 2>/dev/null || echo "calendar database already exists"
|
||||
|
||||
# Create clock database (for clock-backend service)
|
||||
docker compose exec -T postgres psql -U postgres -c "CREATE DATABASE clock;" 2>/dev/null || echo "clock database already exists"
|
||||
|
||||
echo "✅ Databases ready"
|
||||
EOF
|
||||
|
||||
- name: Run database migrations
|
||||
env:
|
||||
STAGING_USER: deploy
|
||||
STAGING_HOST: 46.224.108.214
|
||||
run: |
|
||||
ssh $STAGING_USER@$STAGING_HOST << 'EOF'
|
||||
cd ~/manacore-staging
|
||||
|
||||
echo "=== Running Database Migrations ==="
|
||||
echo ""
|
||||
|
||||
# Migration function with retry logic
|
||||
run_migration() {
|
||||
local service=$1
|
||||
local max_attempts=3
|
||||
local timeout=300 # 5 minutes
|
||||
local attempt=1
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
echo "[$service] Migration attempt $attempt/$max_attempts..."
|
||||
|
||||
# Run migration with timeout
|
||||
if timeout $timeout docker compose exec -T $service pnpm run db:migrate 2>&1; then
|
||||
echo "✅ [$service] Migration succeeded"
|
||||
return 0
|
||||
else
|
||||
exit_code=$?
|
||||
if [ $exit_code -eq 124 ]; then
|
||||
echo "⚠️ [$service] Migration timeout after ${timeout}s"
|
||||
else
|
||||
echo "⚠️ [$service] Migration failed with exit code $exit_code"
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
if [ $attempt -le $max_attempts ]; then
|
||||
wait_time=$((10 * attempt)) # Backoff: 10s, 20s, 30s
|
||||
echo " Waiting ${wait_time}s before retry..."
|
||||
sleep $wait_time
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "❌ [$service] Migration failed after $max_attempts attempts"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Run migrations for services that have db:migrate script
|
||||
# mana-core-auth - central auth service
|
||||
if docker compose exec -T mana-core-auth test -f src/db/migrate.ts 2>/dev/null || \
|
||||
docker compose exec -T mana-core-auth pnpm run db:migrate --help 2>/dev/null; then
|
||||
run_migration mana-core-auth || {
|
||||
echo "❌ mana-core-auth migration failed - aborting deployment"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
echo "⏭️ [mana-core-auth] No db:migrate script, using db:push..."
|
||||
docker compose exec -T mana-core-auth npx drizzle-kit push --force || echo "Auth schema push completed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ All migrations completed"
|
||||
EOF
|
||||
|
||||
- name: Run health checks
|
||||
env:
|
||||
STAGING_USER: deploy
|
||||
|
|
@ -193,66 +274,69 @@ jobs:
|
|||
ssh $STAGING_USER@$STAGING_HOST << 'EOF'
|
||||
cd ~/manacore-staging
|
||||
|
||||
# Wait for services to fully start
|
||||
echo "Waiting 60s for services to fully initialize..."
|
||||
sleep 60
|
||||
echo "=== Health Checks with Polling ==="
|
||||
echo ""
|
||||
|
||||
# Health check function with retry polling
|
||||
check_health() {
|
||||
local service=$1
|
||||
local url=$2
|
||||
local max_attempts=24 # 24 * 5s = 2 minutes max wait
|
||||
local attempt=1
|
||||
|
||||
echo "Checking $service..."
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
# Check if container is running
|
||||
if ! docker compose ps $service 2>/dev/null | grep -q "Up"; then
|
||||
if [ $attempt -eq 1 ]; then
|
||||
echo " ⏳ Waiting for container to start..."
|
||||
fi
|
||||
sleep 5
|
||||
attempt=$((attempt + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check health endpoint
|
||||
if docker compose exec -T $service wget -q -O - $url > /dev/null 2>&1; then
|
||||
echo " ✅ $service is healthy (attempt $attempt)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ $attempt -eq 1 ]; then
|
||||
echo " ⏳ Waiting for $service to become healthy..."
|
||||
fi
|
||||
|
||||
sleep 5
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
echo " ❌ $service health check failed after $max_attempts attempts"
|
||||
echo " === Recent Logs ==="
|
||||
docker compose logs --tail=50 $service
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "=== Container Status ==="
|
||||
docker compose ps
|
||||
|
||||
echo ""
|
||||
echo "=== Health Checks ==="
|
||||
|
||||
# Check mana-core-auth
|
||||
echo "Checking mana-core-auth..."
|
||||
if docker compose exec -T mana-core-auth wget -q -O - http://localhost:3001/api/v1/health > /dev/null 2>&1; then
|
||||
echo "✅ mana-core-auth is healthy"
|
||||
else
|
||||
echo "❌ mana-core-auth health check failed"
|
||||
echo "=== Logs ==="
|
||||
docker compose logs --tail=50 mana-core-auth
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check chat-backend
|
||||
echo "Checking chat-backend..."
|
||||
if docker compose exec -T chat-backend wget -q -O - http://localhost:3002/api/v1/health > /dev/null 2>&1; then
|
||||
echo "✅ chat-backend is healthy"
|
||||
else
|
||||
echo "❌ chat-backend health check failed"
|
||||
echo "=== Logs ==="
|
||||
docker compose logs --tail=50 chat-backend
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check chat-web
|
||||
echo "Checking chat-web..."
|
||||
if docker compose exec -T chat-web wget -q -O - http://localhost:3000/health > /dev/null 2>&1; then
|
||||
echo "✅ chat-web is healthy"
|
||||
else
|
||||
echo "❌ chat-web health check failed"
|
||||
echo "=== Logs ==="
|
||||
docker compose logs --tail=50 chat-web
|
||||
exit 1
|
||||
fi
|
||||
# Check all services with polling
|
||||
check_health mana-core-auth http://localhost:3001/api/v1/health || exit 1
|
||||
check_health chat-backend http://localhost:3002/api/v1/health || exit 1
|
||||
check_health chat-web http://localhost:3000/health || exit 1
|
||||
check_health manacore-web http://localhost:5173/health || exit 1
|
||||
check_health todo-backend http://localhost:3018/api/v1/health || exit 1
|
||||
check_health todo-web http://localhost:5188/health || exit 1
|
||||
check_health calendar-backend http://localhost:3016/api/v1/health || exit 1
|
||||
check_health calendar-web http://localhost:5186/health || exit 1
|
||||
check_health clock-backend http://localhost:3017/api/v1/health || exit 1
|
||||
check_health clock-web http://localhost:5187/health || exit 1
|
||||
|
||||
echo ""
|
||||
echo "✅ All health checks passed!"
|
||||
EOF
|
||||
|
||||
- name: Run database migrations
|
||||
env:
|
||||
STAGING_USER: deploy
|
||||
STAGING_HOST: 46.224.108.214
|
||||
run: |
|
||||
# Run migrations for services that need them
|
||||
ssh $STAGING_USER@$STAGING_HOST << 'EOF'
|
||||
cd ~/manacore-staging
|
||||
|
||||
# Mana Core Auth - push schema using Drizzle (--force skips interactive confirmation)
|
||||
docker compose exec -T mana-core-auth npx drizzle-kit push --force || echo "Auth schema push skipped"
|
||||
EOF
|
||||
|
||||
- name: Deployment summary
|
||||
run: |
|
||||
echo "## Staging Deployment Summary" >> $GITHUB_STEP_SUMMARY
|
||||
|
|
|
|||
|
|
@ -1,31 +1,98 @@
|
|||
# MINIMAL: Only builds mana-core-auth + chat Docker images, no validation
|
||||
# Full config archived at: .github/workflows/ci-main.full.yml
|
||||
# CI Pipeline: Validates code on PRs, builds images on push
|
||||
#
|
||||
# To restore: cp .github/workflows/ci-main.full.yml .github/workflows/ci-main.yml
|
||||
# Flow:
|
||||
# PR → dev/main : Runs validation (required status check)
|
||||
# Push → dev/main : Builds Docker images (NO auto-deploy)
|
||||
#
|
||||
# Deployments are triggered separately:
|
||||
# - Manual: workflow_dispatch on cd-staging.yml / cd-production.yml
|
||||
# - Tag-based: push tag like "chat-staging-v1.0.0" triggers cd-staging-tagged.yml
|
||||
#
|
||||
# Full config archived at: .github/workflows/ci-main.full.yml
|
||||
|
||||
name: CI - Main Branch
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
paths:
|
||||
- 'apps/**'
|
||||
- 'packages/**'
|
||||
- 'services/**'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'turbo.json'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
paths:
|
||||
- 'apps/**'
|
||||
- 'packages/**'
|
||||
- 'services/**'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'turbo.json'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_VERSION: '20'
|
||||
PNPM_VERSION: '9.15.0'
|
||||
|
||||
jobs:
|
||||
# Build Docker images directly - Dockerfiles handle their own dependencies
|
||||
# Validation job - runs on PRs to catch issues before merge
|
||||
validate:
|
||||
name: Validate
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Type check
|
||||
run: pnpm run type-check
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint || echo "Lint warnings found"
|
||||
|
||||
# Build Docker images - only on push to dev/main (not PRs)
|
||||
build-docker-images:
|
||||
name: Build ${{ matrix.service.name }}
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
strategy:
|
||||
matrix:
|
||||
service:
|
||||
- { name: 'mana-core-auth', path: 'services/mana-core-auth', port: '3001' }
|
||||
- { name: 'chat-backend', path: 'apps/chat/apps/backend', port: '3002' }
|
||||
- { name: 'chat-web', path: 'apps/chat/apps/web', port: '3000' }
|
||||
- { name: 'manacore-web', path: 'apps/manacore/apps/web', port: '5173' }
|
||||
- { name: 'todo-backend', path: 'apps/todo/apps/backend', port: '3018' }
|
||||
- { name: 'todo-web', path: 'apps/todo/apps/web', port: '5188' }
|
||||
- { name: 'calendar-backend', path: 'apps/calendar/apps/backend', port: '3016' }
|
||||
- { name: 'calendar-web', path: 'apps/calendar/apps/web', port: '5186' }
|
||||
- { name: 'clock-backend', path: 'apps/clock/apps/backend', port: '3017' }
|
||||
- { name: 'clock-web', path: 'apps/clock/apps/web', port: '5187' }
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
|
@ -73,20 +140,3 @@ jobs:
|
|||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# Trigger staging deployment after all images are built
|
||||
deploy-staging:
|
||||
name: Deploy to Staging
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-docker-images
|
||||
steps:
|
||||
- name: Trigger staging deployment
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'cd-staging.yml',
|
||||
ref: 'main'
|
||||
});
|
||||
console.log('Staging deployment triggered');
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -95,3 +95,4 @@ yarn.lock
|
|||
|
||||
# Claude Flow metrics
|
||||
.claude-flow/
|
||||
.claude-flow/metrics/
|
||||
|
|
|
|||
39
CLAUDE.md
39
CLAUDE.md
|
|
@ -60,19 +60,46 @@ These projects are temporarily archived and excluded from the workspace. To re-a
|
|||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
For detailed local development setup, see **[docs/LOCAL_DEVELOPMENT.md](docs/LOCAL_DEVELOPMENT.md)**.
|
||||
|
||||
### Quick Start (Recommended)
|
||||
|
||||
Use `dev:*:full` commands to start any app with automatic database setup:
|
||||
|
||||
```bash
|
||||
pnpm docker:up # Start PostgreSQL, Redis, MinIO
|
||||
pnpm dev:chat:full # Start chat with auth + auto DB setup
|
||||
pnpm dev:zitare:full # Start zitare with auth + auto DB setup
|
||||
pnpm dev:contacts:full # Start contacts with auth + auto DB setup
|
||||
pnpm dev:calendar:full # Start calendar with auth + auto DB setup
|
||||
pnpm dev:clock:full # Start clock with auth + auto DB setup
|
||||
pnpm dev:todo:full # Start todo with auth + auto DB setup
|
||||
pnpm dev:picture:full # Start picture with auth + auto DB setup
|
||||
```
|
||||
|
||||
These commands automatically:
|
||||
1. Create the database if missing
|
||||
2. Push the latest schema
|
||||
3. Start auth, backend, and web with colored output
|
||||
|
||||
### Database Setup
|
||||
|
||||
```bash
|
||||
pnpm setup:db # Setup ALL databases and schemas
|
||||
pnpm setup:db:chat # Setup just chat
|
||||
pnpm setup:db:auth # Setup just auth
|
||||
```
|
||||
|
||||
### Individual App Commands
|
||||
|
||||
```bash
|
||||
# Start specific project (runs all apps in project)
|
||||
pnpm run manacore:dev
|
||||
pnpm run manadeck:dev
|
||||
pnpm run picture:dev
|
||||
pnpm run chat:dev
|
||||
pnpm run zitare:dev
|
||||
pnpm run presi:dev
|
||||
pnpm run contacts:dev
|
||||
pnpm run mail:dev
|
||||
|
||||
# Start specific app within project
|
||||
pnpm run dev:chat:mobile # Just mobile app
|
||||
|
|
@ -607,7 +634,9 @@ PORT=...
|
|||
|
||||
## Project-Specific Documentation
|
||||
|
||||
- **[docs/LOCAL_DEVELOPMENT.md](docs/LOCAL_DEVELOPMENT.md)** - Database setup and `dev:*:full` commands
|
||||
- **[docs/ENVIRONMENT_VARIABLES.md](docs/ENVIRONMENT_VARIABLES.md)** - Complete environment setup guide
|
||||
- **[docs/DATABASE_MIGRATIONS.md](docs/DATABASE_MIGRATIONS.md)** - Migration best practices, CI/CD, rollback procedures
|
||||
|
||||
Each project has its own `CLAUDE.md` with detailed information:
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ Common issues and solutions for the manacore-monorepo.
|
|||
- [Client-Side Calling localhost Instead of Public IP](#problem-5-client-side-calling-localhost-instead-of-public-ip)
|
||||
- [CORS Blocking Cross-Origin Requests](#problem-6-cors-blocking-cross-origin-requests)
|
||||
- [Missing Database Schema](#problem-7-missing-database-schema)
|
||||
- [pnpm Symlinks Broken in Docker Container](#problem-8-pnpm-symlinks-broken-in-docker-container)
|
||||
- [Hardcoded localhost URLs in SvelteKit Web Apps](#problem-9-hardcoded-localhost-urls-in-sveltekit-web-apps)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -922,6 +924,229 @@ docker compose exec -T chat-backend wget -q -O - http://localhost:3002/api/v1/he
|
|||
|
||||
12. **Drizzle Kit Interactive Mode:** `drizzle-kit push` prompts for confirmation. Use `--force` flag in CI/CD to skip interactive mode.
|
||||
|
||||
13. **pnpm Symlinks in Docker:** pnpm uses symlinks to a central `.pnpm` store. When copying `node_modules` in Docker, you must preserve both the symlinks AND the target directory they point to. See [Problem 8](#problem-8-pnpm-symlinks-broken-in-docker-container).
|
||||
|
||||
---
|
||||
|
||||
### Problem 8: pnpm Symlinks Broken in Docker Container
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Container starts but crashes with: `Cannot find package 'date-fns' imported from /app/build/server/chunks/_page.svelte-xxx.js`
|
||||
- Error `ERR_MODULE_NOT_FOUND` for packages that ARE in node_modules
|
||||
- Works locally but fails in Docker production stage
|
||||
- `ls node_modules/date-fns` shows a symlink pointing to `../../../../../node_modules/.pnpm/...`
|
||||
|
||||
**Root Cause:**
|
||||
|
||||
pnpm uses symlinks to a central `.pnpm` store at the monorepo root. When you copy only the app's `node_modules` in Docker, the symlinks point to paths that don't exist:
|
||||
|
||||
```
|
||||
# In builder stage (pnpm workspace):
|
||||
/app/apps/todo/apps/web/node_modules/date-fns → ../../../../../node_modules/.pnpm/date-fns@4.1.0/node_modules/date-fns
|
||||
|
||||
# In production stage (old broken approach):
|
||||
/app/node_modules/date-fns → ../../../../../node_modules/.pnpm/...
|
||||
# ↑ BROKEN! This path doesn't exist because we only copied to /app/
|
||||
```
|
||||
|
||||
**❌ WRONG - Flattening Directory Structure:**
|
||||
|
||||
```dockerfile
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
WORKDIR /app # ❌ Different from builder structure
|
||||
|
||||
# Copy node_modules (symlinks will be broken!)
|
||||
COPY --from=builder /app/apps/todo/apps/web/node_modules ./node_modules # ❌ BROKEN
|
||||
COPY --from=builder /app/apps/todo/apps/web/build ./build
|
||||
COPY --from=builder /app/apps/todo/apps/web/package.json ./
|
||||
|
||||
CMD ["node", "build"]
|
||||
```
|
||||
|
||||
The symlinks in `node_modules` point to `../../../../../node_modules/.pnpm/...` which resolves to a non-existent path from `/app/`.
|
||||
|
||||
**✅ CORRECT - Preserve Directory Structure + Copy .pnpm Store:**
|
||||
|
||||
```dockerfile
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Keep same directory structure as builder so pnpm symlinks resolve correctly
|
||||
WORKDIR /app/apps/todo/apps/web # ✅ Same as builder
|
||||
|
||||
# Copy the pnpm store that symlinks point to (at /app/node_modules/.pnpm)
|
||||
COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm # ✅ Target of symlinks
|
||||
|
||||
# Copy the app's node_modules (contains symlinks to the pnpm store)
|
||||
COPY --from=builder /app/apps/todo/apps/web/node_modules ./node_modules # ✅ Symlinks work now
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/apps/todo/apps/web/build ./build
|
||||
COPY --from=builder /app/apps/todo/apps/web/package.json ./
|
||||
|
||||
CMD ["node", "build"]
|
||||
```
|
||||
|
||||
**Why This Works:**
|
||||
|
||||
1. `WORKDIR /app/apps/todo/apps/web` - Production container has same path as builder
|
||||
2. Symlinks in `./node_modules/` point to `../../../../../node_modules/.pnpm/...`
|
||||
3. From `/app/apps/todo/apps/web/node_modules/`, going up 5 directories reaches `/app/`
|
||||
4. `/app/node_modules/.pnpm/` exists because we copied it!
|
||||
|
||||
**How to Debug:**
|
||||
|
||||
```bash
|
||||
# Check if symlinks are broken in the container
|
||||
docker exec <container> ls -la node_modules/date-fns
|
||||
# Shows: date-fns -> ../../../../../node_modules/.pnpm/date-fns@4.1.0/node_modules/date-fns
|
||||
|
||||
# Check if the target exists
|
||||
docker exec <container> ls -la /app/node_modules/.pnpm/date-fns@4.1.0/
|
||||
# If "No such file or directory" → symlink is broken
|
||||
|
||||
# Check the image size (should be ~1GB with .pnpm store, ~50MB without)
|
||||
docker images | grep todo-web
|
||||
```
|
||||
|
||||
**Trade-offs:**
|
||||
|
||||
| Approach | Image Size | Symlinks | Works |
|
||||
| ------------------------------------- | ---------- | -------- | ----- |
|
||||
| Copy only app's node_modules | ~50MB | Broken | ❌ |
|
||||
| Copy app's node_modules + .pnpm store | ~1GB | Working | ✅ |
|
||||
|
||||
The larger image size is the cost of pnpm's deduplication strategy. In a monorepo, this is actually more efficient than copying all dependencies flat.
|
||||
|
||||
**Alternative: Use npm Instead of pnpm in Docker:**
|
||||
|
||||
If image size is critical, you could use npm in the Docker build:
|
||||
|
||||
```dockerfile
|
||||
# Alternative approach (not recommended for monorepos)
|
||||
FROM node:20-alpine AS production
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/apps/todo/apps/web/build ./build
|
||||
COPY --from=builder /app/apps/todo/apps/web/package.json ./
|
||||
|
||||
# Clean install with npm (flattens dependencies)
|
||||
RUN npm install --omit=dev
|
||||
|
||||
CMD ["node", "build"]
|
||||
```
|
||||
|
||||
⚠️ **Warning:** This may fail with `workspace:*` protocol in package.json dependencies. Only works if all dependencies are published to npm.
|
||||
|
||||
**Affected Files:**
|
||||
|
||||
- `apps/todo/apps/web/Dockerfile`
|
||||
- `apps/manacore/apps/web/Dockerfile`
|
||||
- `apps/chat/apps/web/Dockerfile`
|
||||
- `apps/calendar/apps/web/Dockerfile`
|
||||
- `apps/clock/apps/web/Dockerfile`
|
||||
|
||||
**Related Commits:**
|
||||
|
||||
- `fd1c0ee6` - fix(docker): preserve pnpm symlink structure in web Dockerfiles
|
||||
|
||||
---
|
||||
|
||||
### Problem 9: Hardcoded localhost URLs in SvelteKit Web Apps
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Browser console shows: `POST http://localhost:3001/api/v1/auth/login net::ERR_CONNECTION_REFUSED`
|
||||
- App works locally but auth fails on staging/production
|
||||
- The `window.__PUBLIC_MANA_CORE_AUTH_URL__` may be set correctly, but code doesn't use it
|
||||
- Looking at the source code reveals hardcoded URLs like `const MANA_AUTH_URL = 'http://localhost:3001'`
|
||||
|
||||
**Root Cause:**
|
||||
|
||||
Developers hardcode `localhost:3001` directly in TypeScript files instead of using the runtime injection pattern. This works locally but breaks in Docker deployments.
|
||||
|
||||
**Common Locations of Hardcoded URLs:**
|
||||
|
||||
```typescript
|
||||
// ❌ These patterns are WRONG:
|
||||
|
||||
// In auth.svelte.ts
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
|
||||
// In user-settings.svelte.ts
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
|
||||
// In feedback.ts or feedback page
|
||||
apiUrl: 'http://localhost:3001',
|
||||
|
||||
// Using build-time env vars (also wrong for Docker)
|
||||
const MANA_AUTH_URL = import.meta.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. **Create `hooks.server.ts`** if it doesn't exist (see Problem 5)
|
||||
2. **Use `getAuthUrl()` pattern in all files:**
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT pattern
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Use getAuthUrl() instead of hardcoded string
|
||||
const auth = initializeWebAuth({ baseUrl: getAuthUrl() });
|
||||
```
|
||||
|
||||
**How to Find Hardcoded URLs:**
|
||||
|
||||
```bash
|
||||
# Search for hardcoded localhost:3001 in web apps
|
||||
grep -r "localhost:3001" apps/*/apps/web/src --include="*.ts" --include="*.svelte"
|
||||
|
||||
# Check for the correct pattern (window injection)
|
||||
grep -r "__PUBLIC_MANA_CORE_AUTH_URL__" apps/*/apps/web/src
|
||||
```
|
||||
|
||||
**Apps Status (as of 2024-12):**
|
||||
|
||||
| App | Status | Files to Fix |
|
||||
| ------------------- | ------------ | ---------------------------------------------------------------- |
|
||||
| `chat/apps/web` | ✅ Fixed | - |
|
||||
| `todo/apps/web` | ✅ Fixed | - |
|
||||
| `calendar/apps/web` | ✅ Fixed | - |
|
||||
| `clock/apps/web` | ✅ Fixed | - |
|
||||
| `contacts/apps/web` | ❌ Needs Fix | auth.svelte.ts, user-settings.svelte.ts, feedback.ts |
|
||||
| `manadeck/apps/web` | ❌ Needs Fix | user-settings.svelte.ts, feedback.ts |
|
||||
| `manacore/apps/web` | ❌ Needs Fix | auth.svelte.ts, user-settings.svelte.ts, feedback.ts, credits.ts |
|
||||
| `zitare/apps/web` | ❌ Needs Fix | auth.svelte.ts, user-settings.svelte.ts, feedback.ts |
|
||||
| `picture/apps/web` | ❌ Needs Fix | user-settings.svelte.ts |
|
||||
|
||||
**Complete Fix Checklist for Each App:**
|
||||
|
||||
- [ ] Create/update `src/hooks.server.ts` with env injection
|
||||
- [ ] Update `src/lib/stores/auth.svelte.ts` → use `getAuthUrl()` pattern
|
||||
- [ ] Update `src/lib/stores/user-settings.svelte.ts` → use `getAuthUrl()` pattern
|
||||
- [ ] Update any `feedback.ts` or feedback services → use `getAuthUrl()` pattern
|
||||
- [ ] Update any other files with hardcoded URLs
|
||||
- [ ] Add `PUBLIC_MANA_CORE_AUTH_URL_CLIENT` to `docker-compose.staging.yml`
|
||||
- [ ] Test locally with `pnpm dev`
|
||||
- [ ] Deploy and test on staging
|
||||
|
||||
**See Also:**
|
||||
|
||||
- [Problem 5: Client-Side Calling localhost Instead of Public IP](#problem-5-client-side-calling-localhost-instead-of-public-ip)
|
||||
- [SvelteKit Web Guidelines - Environment Variables](/.claude/guidelines/sveltekit-web.md#environment-variables)
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
|
|
|||
68
apps/calendar/apps/backend/Dockerfile
Normal file
68
apps/calendar/apps/backend/Dockerfile
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root workspace files
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Copy shared packages
|
||||
COPY packages/shared-errors ./packages/shared-errors
|
||||
COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth
|
||||
|
||||
# Copy calendar packages and backend
|
||||
COPY apps/calendar/packages ./apps/calendar/packages
|
||||
COPY apps/calendar/apps/backend ./apps/calendar/apps/backend
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build shared packages first
|
||||
WORKDIR /app/packages/shared-errors
|
||||
RUN pnpm build
|
||||
|
||||
WORKDIR /app/packages/shared-nestjs-auth
|
||||
RUN pnpm build
|
||||
|
||||
# Build the backend
|
||||
WORKDIR /app/apps/calendar/apps/backend
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Install pnpm and postgresql-client for health checks
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate \
|
||||
&& apk add --no-cache postgresql-client
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy everything from builder (including node_modules)
|
||||
COPY --from=builder /app/pnpm-workspace.yaml ./
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/pnpm-lock.yaml ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/packages ./packages
|
||||
COPY --from=builder /app/apps/calendar ./apps/calendar
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY apps/calendar/apps/backend/docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
WORKDIR /app/apps/calendar/apps/backend
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3016
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3016/api/v1/health || exit 1
|
||||
|
||||
# Run entrypoint script
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["node", "dist/main.js"]
|
||||
23
apps/calendar/apps/backend/docker-entrypoint.sh
Normal file
23
apps/calendar/apps/backend/docker-entrypoint.sh
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "=== Calendar Backend Entrypoint ==="
|
||||
|
||||
# Wait for PostgreSQL to be ready
|
||||
echo "Waiting for PostgreSQL..."
|
||||
until pg_isready -h ${DB_HOST:-postgres} -p ${DB_PORT:-5432} -U ${DB_USER:-postgres} 2>/dev/null; do
|
||||
echo "PostgreSQL is unavailable - sleeping"
|
||||
sleep 2
|
||||
done
|
||||
echo "PostgreSQL is up!"
|
||||
|
||||
cd /app/apps/calendar/apps/backend
|
||||
|
||||
# Run schema push
|
||||
echo "Pushing database schema..."
|
||||
npx drizzle-kit push --force
|
||||
echo "Schema push completed!"
|
||||
|
||||
# Execute the main command
|
||||
echo "Starting application..."
|
||||
exec "$@"
|
||||
84
apps/calendar/apps/web/Dockerfile
Normal file
84
apps/calendar/apps/web/Dockerfile
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Build arguments for SvelteKit static env vars
|
||||
ARG PUBLIC_BACKEND_URL=http://calendar-backend:3016
|
||||
ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-core-auth:3001
|
||||
|
||||
# Set as environment variables for build
|
||||
ENV PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL
|
||||
ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root workspace files
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Copy shared packages needed by calendar web
|
||||
COPY packages/shared-auth ./packages/shared-auth
|
||||
COPY packages/shared-auth-ui ./packages/shared-auth-ui
|
||||
COPY packages/shared-branding ./packages/shared-branding
|
||||
COPY packages/shared-feedback-service ./packages/shared-feedback-service
|
||||
COPY packages/shared-feedback-types ./packages/shared-feedback-types
|
||||
COPY packages/shared-feedback-ui ./packages/shared-feedback-ui
|
||||
COPY packages/shared-i18n ./packages/shared-i18n
|
||||
COPY packages/shared-icons ./packages/shared-icons
|
||||
COPY packages/shared-tailwind ./packages/shared-tailwind
|
||||
COPY packages/shared-theme ./packages/shared-theme
|
||||
COPY packages/shared-theme-ui ./packages/shared-theme-ui
|
||||
COPY packages/shared-subscription-types ./packages/shared-subscription-types
|
||||
COPY packages/shared-subscription-ui ./packages/shared-subscription-ui
|
||||
COPY packages/shared-profile-ui ./packages/shared-profile-ui
|
||||
COPY packages/shared-ui ./packages/shared-ui
|
||||
COPY packages/shared-utils ./packages/shared-utils
|
||||
|
||||
# Copy calendar packages and web
|
||||
COPY apps/calendar/packages ./apps/calendar/packages
|
||||
COPY apps/calendar/apps/web ./apps/calendar/apps/web
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build shared packages that need building
|
||||
WORKDIR /app/packages/shared-auth
|
||||
RUN pnpm build || true
|
||||
|
||||
# Build the web app
|
||||
WORKDIR /app/apps/calendar/apps/web
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Keep same directory structure as builder so pnpm symlinks resolve correctly
|
||||
WORKDIR /app/apps/calendar/apps/web
|
||||
|
||||
# Copy the pnpm store that symlinks point to (at /app/node_modules/.pnpm)
|
||||
COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm
|
||||
|
||||
# Copy the app's node_modules (contains symlinks to the pnpm store)
|
||||
COPY --from=builder /app/apps/calendar/apps/web/node_modules ./node_modules
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/apps/calendar/apps/web/build ./build
|
||||
COPY --from=builder /app/apps/calendar/apps/web/package.json ./
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5186
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=5186
|
||||
ENV HOST=0.0.0.0
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:5186/health || exit 1
|
||||
|
||||
# Run the app
|
||||
CMD ["node", "build"]
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"type-check": "echo 'Skipping type-check for now'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-node": "^5.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
|
|
|
|||
27
apps/calendar/apps/web/src/hooks.server.ts
Normal file
27
apps/calendar/apps/web/src/hooks.server.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Server Hooks for SvelteKit
|
||||
* - Injects runtime environment variables for client-side use
|
||||
* - Auth is handled client-side via Mana Core Auth
|
||||
*/
|
||||
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
// Get client-side URLs from environment (Docker runtime)
|
||||
const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
|
||||
process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || '';
|
||||
const PUBLIC_BACKEND_URL_CLIENT =
|
||||
process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || '';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) => {
|
||||
// Inject runtime environment variables into the HTML
|
||||
// These will be available on window.__PUBLIC_*__ for client-side code
|
||||
const envScript = `<script>
|
||||
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${PUBLIC_MANA_CORE_AUTH_URL_CLIENT}";
|
||||
window.__PUBLIC_BACKEND_URL__ = "${PUBLIC_BACKEND_URL_CLIENT}";
|
||||
</script>`;
|
||||
return html.replace('<head>', `<head>${envScript}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -2,14 +2,22 @@
|
|||
* Feedback Service Instance for Calendar Web App
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createFeedbackService } from '@manacore/shared-feedback-service';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public';
|
||||
|
||||
const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const feedbackService = createFeedbackService({
|
||||
apiUrl: MANA_AUTH_URL,
|
||||
apiUrl: getAuthUrl(),
|
||||
appId: 'calendar',
|
||||
getAuthToken: async () => authStore.getAccessToken(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,8 +7,18 @@ import { browser } from '$app/environment';
|
|||
import { initializeWebAuth } from '@manacore/shared-auth';
|
||||
import type { UserData } from '@manacore/shared-auth';
|
||||
|
||||
// Initialize Mana Core Auth only on the client side
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime - fallback for SSR and client
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
// Client-side: use injected window variable (set by hooks.server.ts)
|
||||
// Falls back to localhost for local development
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
// Server-side (SSR): use Docker internal URL for container-to-container communication
|
||||
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Lazy initialization to avoid SSR issues with localStorage
|
||||
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
|
||||
|
|
@ -17,7 +27,7 @@ let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null =
|
|||
function getAuthService() {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
|
||||
const auth = initializeWebAuth({ baseUrl: getAuthUrl() });
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,22 @@
|
|||
* - localStorage caching for offline support
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createUserSettingsStore } from '@manacore/shared-theme';
|
||||
import { authStore } from './auth.svelte';
|
||||
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const userSettings = createUserSettingsStore({
|
||||
appId: 'calendar',
|
||||
authUrl: MANA_AUTH_URL,
|
||||
authUrl: getAuthUrl(),
|
||||
getAccessToken: () => authStore.getAccessToken(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,52 +32,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Kalender"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isSidebarMode}
|
||||
onModeChange={handleModeChange}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
desktopPosition={userSettings.nav.desktopPosition}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={authStore.isAuthenticated}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#3b82f6"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
/>
|
||||
|
||||
<main
|
||||
class="main-content bg-background"
|
||||
class:sidebar-mode={isSidebarMode && !isCollapsed}
|
||||
class:floating-mode={!isSidebarMode && !isCollapsed}
|
||||
>
|
||||
<div
|
||||
class="content-wrapper"
|
||||
class:calendar-expanded={settingsStore.sidebarCollapsed && $page.url.pathname === '/'}
|
||||
>
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
<div class="min-h-screen bg-background text-foreground">
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import adapter from '@sveltejs/adapter-auto';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
adapter: adapter({
|
||||
out: 'build',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Build arguments for SvelteKit static env vars
|
||||
ARG PUBLIC_BACKEND_URL=http://chat-backend:3002
|
||||
ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-core-auth:3001
|
||||
|
||||
# Set as environment variables for build
|
||||
ENV PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL
|
||||
ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
|
|
@ -47,15 +55,19 @@ RUN pnpm build
|
|||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
WORKDIR /app
|
||||
# Keep same directory structure as builder so pnpm symlinks resolve correctly
|
||||
WORKDIR /app/apps/chat/apps/web
|
||||
|
||||
# Copy the pnpm store that symlinks point to (at /app/node_modules/.pnpm)
|
||||
COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm
|
||||
|
||||
# Copy the app's node_modules (contains symlinks to the pnpm store)
|
||||
COPY --from=builder /app/apps/chat/apps/web/node_modules ./node_modules
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/apps/chat/apps/web/build ./build
|
||||
COPY --from=builder /app/apps/chat/apps/web/package.json ./
|
||||
|
||||
# Install only production dependencies for the built app
|
||||
RUN npm install --omit=dev 2>/dev/null || true
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
|
|
|
|||
68
apps/clock/apps/backend/Dockerfile
Normal file
68
apps/clock/apps/backend/Dockerfile
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root workspace files
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Copy shared packages
|
||||
COPY packages/shared-errors ./packages/shared-errors
|
||||
COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth
|
||||
|
||||
# Copy clock packages and backend
|
||||
COPY apps/clock/packages ./apps/clock/packages
|
||||
COPY apps/clock/apps/backend ./apps/clock/apps/backend
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build shared packages first
|
||||
WORKDIR /app/packages/shared-errors
|
||||
RUN pnpm build
|
||||
|
||||
WORKDIR /app/packages/shared-nestjs-auth
|
||||
RUN pnpm build
|
||||
|
||||
# Build the backend
|
||||
WORKDIR /app/apps/clock/apps/backend
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Install pnpm and postgresql-client for health checks
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate \
|
||||
&& apk add --no-cache postgresql-client
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy everything from builder (including node_modules)
|
||||
COPY --from=builder /app/pnpm-workspace.yaml ./
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/pnpm-lock.yaml ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/packages ./packages
|
||||
COPY --from=builder /app/apps/clock ./apps/clock
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY apps/clock/apps/backend/docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
WORKDIR /app/apps/clock/apps/backend
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3017
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3017/api/v1/health || exit 1
|
||||
|
||||
# Run entrypoint script
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["node", "dist/main.js"]
|
||||
23
apps/clock/apps/backend/docker-entrypoint.sh
Normal file
23
apps/clock/apps/backend/docker-entrypoint.sh
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "=== Clock Backend Entrypoint ==="
|
||||
|
||||
# Wait for PostgreSQL to be ready
|
||||
echo "Waiting for PostgreSQL..."
|
||||
until pg_isready -h ${DB_HOST:-postgres} -p ${DB_PORT:-5432} -U ${DB_USER:-postgres} 2>/dev/null; do
|
||||
echo "PostgreSQL is unavailable - sleeping"
|
||||
sleep 2
|
||||
done
|
||||
echo "PostgreSQL is up!"
|
||||
|
||||
cd /app/apps/clock/apps/backend
|
||||
|
||||
# Run schema push
|
||||
echo "Pushing database schema..."
|
||||
npx drizzle-kit push --force
|
||||
echo "Schema push completed!"
|
||||
|
||||
# Execute the main command
|
||||
echo "Starting application..."
|
||||
exec "$@"
|
||||
84
apps/clock/apps/web/Dockerfile
Normal file
84
apps/clock/apps/web/Dockerfile
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Build arguments for SvelteKit static env vars
|
||||
ARG PUBLIC_BACKEND_URL=http://clock-backend:3017
|
||||
ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-core-auth:3001
|
||||
|
||||
# Set as environment variables for build
|
||||
ENV PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL
|
||||
ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root workspace files
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Copy shared packages needed by clock web
|
||||
COPY packages/shared-auth ./packages/shared-auth
|
||||
COPY packages/shared-auth-ui ./packages/shared-auth-ui
|
||||
COPY packages/shared-branding ./packages/shared-branding
|
||||
COPY packages/shared-feedback-service ./packages/shared-feedback-service
|
||||
COPY packages/shared-feedback-types ./packages/shared-feedback-types
|
||||
COPY packages/shared-feedback-ui ./packages/shared-feedback-ui
|
||||
COPY packages/shared-i18n ./packages/shared-i18n
|
||||
COPY packages/shared-icons ./packages/shared-icons
|
||||
COPY packages/shared-tailwind ./packages/shared-tailwind
|
||||
COPY packages/shared-theme ./packages/shared-theme
|
||||
COPY packages/shared-theme-ui ./packages/shared-theme-ui
|
||||
COPY packages/shared-subscription-types ./packages/shared-subscription-types
|
||||
COPY packages/shared-subscription-ui ./packages/shared-subscription-ui
|
||||
COPY packages/shared-profile-ui ./packages/shared-profile-ui
|
||||
COPY packages/shared-ui ./packages/shared-ui
|
||||
COPY packages/shared-utils ./packages/shared-utils
|
||||
|
||||
# Copy clock packages and web
|
||||
COPY apps/clock/packages ./apps/clock/packages
|
||||
COPY apps/clock/apps/web ./apps/clock/apps/web
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build shared packages that need building
|
||||
WORKDIR /app/packages/shared-auth
|
||||
RUN pnpm build || true
|
||||
|
||||
# Build the web app
|
||||
WORKDIR /app/apps/clock/apps/web
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Keep same directory structure as builder so pnpm symlinks resolve correctly
|
||||
WORKDIR /app/apps/clock/apps/web
|
||||
|
||||
# Copy the pnpm store that symlinks point to (at /app/node_modules/.pnpm)
|
||||
COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm
|
||||
|
||||
# Copy the app's node_modules (contains symlinks to the pnpm store)
|
||||
COPY --from=builder /app/apps/clock/apps/web/node_modules ./node_modules
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/apps/clock/apps/web/build ./build
|
||||
COPY --from=builder /app/apps/clock/apps/web/package.json ./
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5187
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=5187
|
||||
ENV HOST=0.0.0.0
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:5187/health || exit 1
|
||||
|
||||
# Run the app
|
||||
CMD ["node", "build"]
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"type-check": "echo 'Skipping type-check for now'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-node": "^5.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
|
|
|
|||
27
apps/clock/apps/web/src/hooks.server.ts
Normal file
27
apps/clock/apps/web/src/hooks.server.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Server Hooks for SvelteKit
|
||||
* - Injects runtime environment variables for client-side use
|
||||
* - Auth is handled client-side via Mana Core Auth
|
||||
*/
|
||||
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
// Get client-side URLs from environment (Docker runtime)
|
||||
const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
|
||||
process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || '';
|
||||
const PUBLIC_BACKEND_URL_CLIENT =
|
||||
process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || '';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) => {
|
||||
// Inject runtime environment variables into the HTML
|
||||
// These will be available on window.__PUBLIC_*__ for client-side code
|
||||
const envScript = `<script>
|
||||
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${PUBLIC_MANA_CORE_AUTH_URL_CLIENT}";
|
||||
window.__PUBLIC_BACKEND_URL__ = "${PUBLIC_BACKEND_URL_CLIENT}";
|
||||
</script>`;
|
||||
return html.replace('<head>', `<head>${envScript}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -6,8 +6,18 @@
|
|||
import { browser } from '$app/environment';
|
||||
import { initializeWebAuth, type UserData } from '@manacore/shared-auth';
|
||||
|
||||
// Initialize Mana Core Auth only on the client side
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime - fallback for SSR and client
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
// Client-side: use injected window variable (set by hooks.server.ts)
|
||||
// Falls back to localhost for local development
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
// Server-side (SSR): use Docker internal URL for container-to-container communication
|
||||
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Lazy initialization to avoid SSR issues with localStorage
|
||||
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
|
||||
|
|
@ -16,7 +26,7 @@ let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null =
|
|||
function getAuthService() {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
|
||||
const auth = initializeWebAuth({ baseUrl: getAuthUrl() });
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,22 @@
|
|||
* - localStorage caching for offline support
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createUserSettingsStore } from '@manacore/shared-theme';
|
||||
import { authStore } from './auth.svelte';
|
||||
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const userSettings = createUserSettingsStore({
|
||||
appId: 'clock',
|
||||
authUrl: MANA_AUTH_URL,
|
||||
authUrl: getAuthUrl(),
|
||||
getAccessToken: () => authStore.getAccessToken(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { FeedbackPage } from '@manacore/shared-feedback-ui';
|
||||
import { createFeedbackService } from '@manacore/shared-feedback-service';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import '$lib/i18n';
|
||||
|
||||
// Get auth URL dynamically at runtime
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
const feedbackService = createFeedbackService({
|
||||
appName: 'clock',
|
||||
apiUrl: 'http://localhost:3001', // Mana Core API
|
||||
apiUrl: getAuthUrl(),
|
||||
});
|
||||
|
||||
async function handleSubmit(data: { type: string; message: string; email?: string }) {
|
||||
|
|
|
|||
|
|
@ -1,34 +1,30 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { LoginPage } from '@manacore/shared-auth-ui';
|
||||
import { ClockLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import '$lib/i18n';
|
||||
|
||||
let error = $state('');
|
||||
let loading = $state(false);
|
||||
// Get redirect URL from query params
|
||||
const redirectTo = $derived($page.url.searchParams.get('redirectTo') || '/');
|
||||
|
||||
async function handleLogin(email: string, password: string) {
|
||||
loading = true;
|
||||
error = '';
|
||||
|
||||
const result = await authStore.signIn(email, password);
|
||||
|
||||
if (result.success) {
|
||||
goto('/');
|
||||
} else {
|
||||
error = result.error || 'Login fehlgeschlagen';
|
||||
}
|
||||
|
||||
loading = false;
|
||||
async function handleSignIn(email: string, password: string) {
|
||||
return authStore.signIn(email, password);
|
||||
}
|
||||
</script>
|
||||
|
||||
<LoginPage
|
||||
appName="Clock"
|
||||
appLogo=""
|
||||
{loading}
|
||||
{error}
|
||||
onSubmit={handleLogin}
|
||||
registerHref="/register"
|
||||
forgotPasswordHref="/forgot-password"
|
||||
logo={ClockLogo}
|
||||
primaryColor="#f59e0b"
|
||||
onSignIn={handleSignIn}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
lightBackground="#fef3c7"
|
||||
darkBackground="#1c1917"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import adapter from '@sveltejs/adapter-auto';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
adapter: adapter({
|
||||
out: 'build',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -50,9 +50,9 @@ export const BatchDocumentCreator: React.FC<BatchDocumentCreatorProps> = ({
|
|||
const [subjectList, setSubjectList] = useState<string[]>([]);
|
||||
const [documents, setDocuments] = useState<Document[]>([]);
|
||||
const [selectedDocuments, setSelectedDocuments] = useState<string[]>([]);
|
||||
const [documentFilter, setDocumentFilter] = useState<
|
||||
'all' | 'text' | 'context' | 'prompt'
|
||||
>('context');
|
||||
const [documentFilter, setDocumentFilter] = useState<'all' | 'text' | 'context' | 'prompt'>(
|
||||
'context'
|
||||
);
|
||||
const [promptDocuments, setPromptDocuments] = useState<Document[]>([]);
|
||||
const { mode, colors } = useTheme();
|
||||
const isDark = mode === 'dark';
|
||||
|
|
|
|||
|
|
@ -216,9 +216,7 @@ export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
|
|||
? 'font-medium text-gray-800 dark:text-gray-200'
|
||||
: 'text-gray-500 dark:text-gray-400'
|
||||
)}
|
||||
style={[
|
||||
pressed && !isLast && styles.textHovered,
|
||||
]}
|
||||
style={[pressed && !isLast && styles.textHovered]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ export const FilterPill: React.FC<FilterPillProps> = ({
|
|||
: isDark
|
||||
? '#1f2937'
|
||||
: '#d1d5db',
|
||||
opacity: disabled ? 0.6 : (pressed ? 0.8 : 1),
|
||||
opacity: disabled ? 0.6 : pressed ? 0.8 : 1,
|
||||
},
|
||||
]}
|
||||
onPress={disabled ? undefined : actionButton.onPress}
|
||||
|
|
|
|||
85
apps/manacore/apps/web/Dockerfile
Normal file
85
apps/manacore/apps/web/Dockerfile
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Build arguments for SvelteKit static env vars
|
||||
ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-core-auth:3001
|
||||
ARG MIDDLEWARE_URL=http://mana-core-middleware:3000
|
||||
|
||||
# Set as environment variables for build
|
||||
ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL
|
||||
ENV MIDDLEWARE_URL=$MIDDLEWARE_URL
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root workspace files
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Copy shared packages needed by manacore web
|
||||
COPY packages/shared-auth ./packages/shared-auth
|
||||
COPY packages/shared-auth-ui ./packages/shared-auth-ui
|
||||
COPY packages/shared-branding ./packages/shared-branding
|
||||
COPY packages/shared-config ./packages/shared-config
|
||||
COPY packages/shared-feedback-service ./packages/shared-feedback-service
|
||||
COPY packages/shared-feedback-types ./packages/shared-feedback-types
|
||||
COPY packages/shared-feedback-ui ./packages/shared-feedback-ui
|
||||
COPY packages/shared-i18n ./packages/shared-i18n
|
||||
COPY packages/shared-icons ./packages/shared-icons
|
||||
COPY packages/shared-tailwind ./packages/shared-tailwind
|
||||
COPY packages/shared-theme ./packages/shared-theme
|
||||
COPY packages/shared-theme-ui ./packages/shared-theme-ui
|
||||
COPY packages/shared-subscription-types ./packages/shared-subscription-types
|
||||
COPY packages/shared-subscription-ui ./packages/shared-subscription-ui
|
||||
COPY packages/shared-profile-ui ./packages/shared-profile-ui
|
||||
COPY packages/shared-types ./packages/shared-types
|
||||
COPY packages/shared-ui ./packages/shared-ui
|
||||
COPY packages/shared-utils ./packages/shared-utils
|
||||
|
||||
# Copy manacore web
|
||||
COPY apps/manacore/apps/web ./apps/manacore/apps/web
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build shared packages that need building
|
||||
WORKDIR /app/packages/shared-auth
|
||||
RUN pnpm build || true
|
||||
|
||||
# Build the web app
|
||||
WORKDIR /app/apps/manacore/apps/web
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Keep same directory structure as builder so pnpm symlinks resolve correctly
|
||||
WORKDIR /app/apps/manacore/apps/web
|
||||
|
||||
# Copy the pnpm store that symlinks point to (at /app/node_modules/.pnpm)
|
||||
COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm
|
||||
|
||||
# Copy the app's node_modules (contains symlinks to the pnpm store)
|
||||
COPY --from=builder /app/apps/manacore/apps/web/node_modules ./node_modules
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/apps/manacore/apps/web/build ./build
|
||||
COPY --from=builder /app/apps/manacore/apps/web/package.json ./
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5173
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=5173
|
||||
ENV HOST=0.0.0.0
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:5173/health || exit 1
|
||||
|
||||
# Run the app
|
||||
CMD ["node", "build"]
|
||||
|
|
@ -16,8 +16,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.51.0",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/adapter-netlify": "^5.2.4",
|
||||
"@sveltejs/adapter-node": "^5.0.0",
|
||||
"@sveltejs/kit": "^2.15.7",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.4",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
|
|
@ -50,15 +49,12 @@
|
|||
"@manacore/shared-profile-ui": "workspace:*",
|
||||
"@manacore/shared-subscription-types": "workspace:*",
|
||||
"@manacore/shared-subscription-ui": "workspace:*",
|
||||
"@manacore/shared-supabase": "workspace:*",
|
||||
"@manacore/shared-tailwind": "workspace:*",
|
||||
"@manacore/shared-theme": "workspace:*",
|
||||
"@manacore/shared-theme-ui": "workspace:*",
|
||||
"@manacore/shared-types": "workspace:*",
|
||||
"@manacore/shared-ui": "workspace:*",
|
||||
"@manacore/shared-utils": "workspace:*",
|
||||
"@supabase/ssr": "^0.5.2",
|
||||
"@supabase/supabase-js": "^2.81.1",
|
||||
"svelte-dnd-action": "^0.9.68",
|
||||
"svelte-i18n": "^4.0.0"
|
||||
},
|
||||
|
|
|
|||
23
apps/manacore/apps/web/src/app.d.ts
vendored
23
apps/manacore/apps/web/src/app.d.ts
vendored
|
|
@ -1,18 +1,15 @@
|
|||
import type { Session, SupabaseClient, User } from '@supabase/supabase-js';
|
||||
|
||||
/**
|
||||
* App type declarations for ManaCore web app
|
||||
*
|
||||
* Authentication is handled entirely by Mana Core Auth (@manacore/shared-auth).
|
||||
* No Supabase is needed - all data comes from mana-core-auth APIs.
|
||||
*/
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
supabase: SupabaseClient;
|
||||
safeGetSession: () => Promise<{ session: Session | null; user: User | null }>;
|
||||
session: Session | null;
|
||||
user: User | null;
|
||||
}
|
||||
interface PageData {
|
||||
// Auth is handled by Mana Core Auth (@manacore/shared-auth), not Supabase
|
||||
// Supabase is used for database operations only
|
||||
supabase?: SupabaseClient;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface Locals {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface PageData {}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,38 @@
|
|||
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
|
||||
import { createServerClient } from '@supabase/ssr';
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
/**
|
||||
* Server hooks for ManaCore web app
|
||||
*
|
||||
* Note: Authentication is handled client-side via Mana Core Auth.
|
||||
* Supabase is only used for database operations (not auth).
|
||||
* Injects runtime environment variables into the HTML for client-side access.
|
||||
* This is necessary because SvelteKit's $env/static/public bakes values at build time,
|
||||
* but Docker containers need runtime configuration.
|
||||
*/
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// Create Supabase client for database operations only
|
||||
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
cookies: {
|
||||
getAll: () => event.cookies.getAll(),
|
||||
setAll: (cookiesToSet) => {
|
||||
cookiesToSet.forEach(({ name, value, options }) => {
|
||||
event.cookies.set(name, value, { ...options, path: '/' });
|
||||
});
|
||||
},
|
||||
},
|
||||
}) as any;
|
||||
|
||||
// Auth URL
|
||||
const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
|
||||
process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || '';
|
||||
|
||||
// Backend URLs for dashboard widgets
|
||||
const PUBLIC_TODO_API_URL_CLIENT =
|
||||
process.env.PUBLIC_TODO_API_URL_CLIENT || process.env.PUBLIC_TODO_API_URL || '';
|
||||
const PUBLIC_CALENDAR_API_URL_CLIENT =
|
||||
process.env.PUBLIC_CALENDAR_API_URL_CLIENT || process.env.PUBLIC_CALENDAR_API_URL || '';
|
||||
const PUBLIC_CLOCK_API_URL_CLIENT =
|
||||
process.env.PUBLIC_CLOCK_API_URL_CLIENT || process.env.PUBLIC_CLOCK_API_URL || '';
|
||||
const PUBLIC_CONTACTS_API_URL_CLIENT =
|
||||
process.env.PUBLIC_CONTACTS_API_URL_CLIENT || process.env.PUBLIC_CONTACTS_API_URL || '';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
return resolve(event, {
|
||||
filterSerializedResponseHeaders(name) {
|
||||
return name === 'content-range' || name === 'x-supabase-api-version';
|
||||
transformPageChunk: ({ html }) => {
|
||||
const envScript = `<script>
|
||||
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${PUBLIC_MANA_CORE_AUTH_URL_CLIENT}";
|
||||
window.__PUBLIC_TODO_API_URL__ = "${PUBLIC_TODO_API_URL_CLIENT}";
|
||||
window.__PUBLIC_CALENDAR_API_URL__ = "${PUBLIC_CALENDAR_API_URL_CLIENT}";
|
||||
window.__PUBLIC_CLOCK_API_URL__ = "${PUBLIC_CLOCK_API_URL_CLIENT}";
|
||||
window.__PUBLIC_CONTACTS_API_URL__ = "${PUBLIC_CONTACTS_API_URL_CLIENT}";
|
||||
</script>`;
|
||||
return html.replace('<head>', `<head>${envScript}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,12 +4,32 @@
|
|||
* Fetches events from the Calendar backend for dashboard widgets.
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createApiClient, type ApiResult } from '../base-client';
|
||||
|
||||
// Backend URL - falls back to localhost for development
|
||||
const CALENDAR_API_URL = import.meta.env.PUBLIC_CALENDAR_API_URL || 'http://localhost:3014/api/v1';
|
||||
// Get Calendar API URL dynamically at runtime
|
||||
function getCalendarApiUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
// Client-side: use injected window variable (set by hooks.server.ts)
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_CALENDAR_API_URL__?: string })
|
||||
.__PUBLIC_CALENDAR_API_URL__;
|
||||
if (injectedUrl) {
|
||||
return `${injectedUrl}/api/v1`;
|
||||
}
|
||||
}
|
||||
// Fallback for local development
|
||||
return 'http://localhost:3016/api/v1';
|
||||
}
|
||||
|
||||
const client = createApiClient(CALENDAR_API_URL);
|
||||
// Lazy-initialized client to ensure we get the correct URL at runtime
|
||||
let _client: ReturnType<typeof createApiClient> | null = null;
|
||||
|
||||
function getClient() {
|
||||
if (!_client) {
|
||||
_client = createApiClient(getCalendarApiUrl());
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calendar entity from Calendar backend
|
||||
|
|
@ -59,7 +79,7 @@ export const calendarService = {
|
|||
const startDate = new Date().toISOString().split('T')[0];
|
||||
const endDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||
|
||||
const result = await client.get<{ events: CalendarEvent[] }>(
|
||||
const result = await getClient().get<{ events: CalendarEvent[] }>(
|
||||
`/events?startDate=${startDate}&endDate=${endDate}`
|
||||
);
|
||||
|
||||
|
|
@ -75,7 +95,7 @@ export const calendarService = {
|
|||
*/
|
||||
async getTodayEvents(): Promise<ApiResult<CalendarEvent[]>> {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const result = await client.get<{ events: CalendarEvent[] }>(
|
||||
const result = await getClient().get<{ events: CalendarEvent[] }>(
|
||||
`/events?startDate=${today}&endDate=${today}`
|
||||
);
|
||||
|
||||
|
|
@ -90,7 +110,7 @@ export const calendarService = {
|
|||
* Get all calendars
|
||||
*/
|
||||
async getCalendars(): Promise<ApiResult<Calendar[]>> {
|
||||
const result = await client.get<{ calendars: Calendar[] }>('/calendars');
|
||||
const result = await getClient().get<{ calendars: Calendar[] }>('/calendars');
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
|
|
@ -109,7 +129,7 @@ export const calendarService = {
|
|||
const startDate = new Date().toISOString().split('T')[0];
|
||||
const endDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||
|
||||
const result = await client.get<{ events: CalendarEvent[] }>(
|
||||
const result = await getClient().get<{ events: CalendarEvent[] }>(
|
||||
`/events?calendarIds=${calendarId}&startDate=${startDate}&endDate=${endDate}`
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,32 @@
|
|||
* Fetches contacts from the Contacts backend for dashboard widgets.
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createApiClient, type ApiResult } from '../base-client';
|
||||
|
||||
// Backend URL - falls back to localhost for development
|
||||
const CONTACTS_API_URL = import.meta.env.PUBLIC_CONTACTS_API_URL || 'http://localhost:3015/api/v1';
|
||||
// Get Contacts API URL dynamically at runtime
|
||||
function getContactsApiUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
// Client-side: use injected window variable (set by hooks.server.ts)
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_CONTACTS_API_URL__?: string })
|
||||
.__PUBLIC_CONTACTS_API_URL__;
|
||||
if (injectedUrl) {
|
||||
return `${injectedUrl}/api/v1`;
|
||||
}
|
||||
}
|
||||
// Fallback for local development
|
||||
return 'http://localhost:3015/api/v1';
|
||||
}
|
||||
|
||||
const client = createApiClient(CONTACTS_API_URL);
|
||||
// Lazy-initialized client to ensure we get the correct URL at runtime
|
||||
let _client: ReturnType<typeof createApiClient> | null = null;
|
||||
|
||||
function getClient() {
|
||||
if (!_client) {
|
||||
_client = createApiClient(getContactsApiUrl());
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contact entity from Contacts backend
|
||||
|
|
@ -55,7 +75,7 @@ export const contactsService = {
|
|||
* Get favorite contacts
|
||||
*/
|
||||
async getFavoriteContacts(limit: number = 5): Promise<ApiResult<Contact[]>> {
|
||||
const result = await client.get<Contact[]>(`/contacts?isFavorite=true&limit=${limit}`);
|
||||
const result = await getClient().get<Contact[]>(`/contacts?isFavorite=true&limit=${limit}`);
|
||||
return result;
|
||||
},
|
||||
|
||||
|
|
@ -63,7 +83,7 @@ export const contactsService = {
|
|||
* Get recent contacts (by updatedAt)
|
||||
*/
|
||||
async getRecentContacts(limit: number = 5): Promise<ApiResult<Contact[]>> {
|
||||
const result = await client.get<Contact[]>(`/contacts?limit=${limit}`);
|
||||
const result = await getClient().get<Contact[]>(`/contacts?limit=${limit}`);
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return result;
|
||||
|
|
@ -82,7 +102,7 @@ export const contactsService = {
|
|||
* Get contacts with upcoming birthdays
|
||||
*/
|
||||
async getUpcomingBirthdays(days: number = 30): Promise<ApiResult<Contact[]>> {
|
||||
const result = await client.get<Contact[]>('/contacts');
|
||||
const result = await getClient().get<Contact[]>('/contacts');
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return result;
|
||||
|
|
@ -113,7 +133,7 @@ export const contactsService = {
|
|||
* Get contact count
|
||||
*/
|
||||
async getContactCount(): Promise<ApiResult<{ total: number; favorites: number }>> {
|
||||
const result = await client.get<Contact[]>('/contacts');
|
||||
const result = await getClient().get<Contact[]>('/contacts');
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
|
|
|
|||
|
|
@ -4,12 +4,32 @@
|
|||
* Fetches tasks from the Todo backend for dashboard widgets.
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createApiClient, type ApiResult } from '../base-client';
|
||||
|
||||
// Backend URL - falls back to localhost for development
|
||||
const TODO_API_URL = import.meta.env.PUBLIC_TODO_API_URL || 'http://localhost:3017/api/v1';
|
||||
// Get Todo API URL dynamically at runtime
|
||||
function getTodoApiUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
// Client-side: use injected window variable (set by hooks.server.ts)
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_TODO_API_URL__?: string })
|
||||
.__PUBLIC_TODO_API_URL__;
|
||||
if (injectedUrl) {
|
||||
return `${injectedUrl}/api/v1`;
|
||||
}
|
||||
}
|
||||
// Fallback for local development
|
||||
return 'http://localhost:3018/api/v1';
|
||||
}
|
||||
|
||||
const client = createApiClient(TODO_API_URL);
|
||||
// Lazy-initialized client to ensure we get the correct URL at runtime
|
||||
let _client: ReturnType<typeof createApiClient> | null = null;
|
||||
|
||||
function getClient() {
|
||||
if (!_client) {
|
||||
_client = createApiClient(getTodoApiUrl());
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task entity from Todo backend
|
||||
|
|
@ -49,7 +69,7 @@ export const todoService = {
|
|||
* Get today's tasks
|
||||
*/
|
||||
async getTodayTasks(): Promise<ApiResult<Task[]>> {
|
||||
const result = await client.get<{ tasks: Task[] }>('/tasks/today');
|
||||
const result = await getClient().get<{ tasks: Task[] }>('/tasks/today');
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
|
|
@ -62,7 +82,7 @@ export const todoService = {
|
|||
* Get upcoming tasks for the next N days
|
||||
*/
|
||||
async getUpcomingTasks(days: number = 7): Promise<ApiResult<Task[]>> {
|
||||
const result = await client.get<{ tasks: Task[] }>(`/tasks/upcoming?days=${days}`);
|
||||
const result = await getClient().get<{ tasks: Task[] }>(`/tasks/upcoming?days=${days}`);
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
|
|
@ -75,7 +95,7 @@ export const todoService = {
|
|||
* Get inbox tasks (unassigned to project)
|
||||
*/
|
||||
async getInboxTasks(): Promise<ApiResult<Task[]>> {
|
||||
const result = await client.get<{ tasks: Task[] }>('/tasks/inbox');
|
||||
const result = await getClient().get<{ tasks: Task[] }>('/tasks/inbox');
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
|
|
@ -88,7 +108,7 @@ export const todoService = {
|
|||
* Get all projects
|
||||
*/
|
||||
async getProjects(): Promise<ApiResult<Project[]>> {
|
||||
const result = await client.get<{ projects: Project[] }>('/projects');
|
||||
const result = await getClient().get<{ projects: Project[] }>('/projects');
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
import type { RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
export async function getUser(event: RequestEvent) {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getUser();
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching user:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function getSession(event: RequestEvent) {
|
||||
const {
|
||||
data: { session },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getSession();
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching session:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function requireAuth(event: RequestEvent) {
|
||||
const session = await getSession(event);
|
||||
|
||||
if (!session) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export function getSupabaseServerClient(event: RequestEvent) {
|
||||
return event.locals.supabase;
|
||||
}
|
||||
|
|
@ -7,9 +7,18 @@ import { browser } from '$app/environment';
|
|||
import { initializeWebAuth } from '@manacore/shared-auth';
|
||||
import type { UserData } from '@manacore/shared-auth';
|
||||
|
||||
// Initialize Mana Core Auth only on the client side
|
||||
// TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env when available
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime - fallback for SSR and client
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
// Client-side: use injected window variable (set by hooks.server.ts)
|
||||
// Falls back to localhost for local development
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
// Server-side (SSR): use Docker internal URL for container-to-container communication
|
||||
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Lazy initialization to avoid SSR issues with localStorage
|
||||
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
|
||||
|
|
@ -18,7 +27,7 @@ let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null =
|
|||
function getAuthService() {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
|
||||
const auth = initializeWebAuth({ baseUrl: getAuthUrl() });
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
|
|
@ -183,6 +192,29 @@ export const authStore = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset password with token
|
||||
*/
|
||||
async resetPassword(token: string, newPassword: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return { success: false, error: 'Auth not available on server' };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await authService.resetPassword(token, newPassword);
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Password reset failed' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get access token for API calls
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,14 +7,22 @@
|
|||
* - localStorage caching for offline support
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createUserSettingsStore } from '@manacore/shared-theme';
|
||||
import { authStore } from './auth.svelte';
|
||||
|
||||
// TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env when available
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const userSettings = createUserSettingsStore({
|
||||
appId: 'manacore',
|
||||
authUrl: MANA_AUTH_URL,
|
||||
authUrl: getAuthUrl(),
|
||||
getAccessToken: () => authStore.getAccessToken(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Button, Input, Card, PageHeader } from '@manacore/shared-ui';
|
||||
import { authStore } from '$lib/stores/authStore.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { creditsService } from '$lib/api/credits';
|
||||
import type { CreditBalance } from '$lib/api/credits';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
import { fail } from '@sveltejs/kit';
|
||||
import type { Actions } from './$types';
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, locals: { supabase }, url }) => {
|
||||
const formData = await request.formData();
|
||||
const email = formData.get('email') as string;
|
||||
|
||||
if (!email) {
|
||||
return fail(400, {
|
||||
error: 'Email is required',
|
||||
email,
|
||||
});
|
||||
}
|
||||
|
||||
// Get the origin for the redirect URL
|
||||
const origin = url.origin;
|
||||
const redirectTo = `${origin}/auth/reset-password`;
|
||||
|
||||
// Send password reset email
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Password reset error:', error);
|
||||
return fail(400, {
|
||||
error: error.message,
|
||||
email,
|
||||
});
|
||||
}
|
||||
|
||||
// Return success (we don't reveal if the email exists for security)
|
||||
return {
|
||||
success: true,
|
||||
email,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import { redirect, fail } from '@sveltejs/kit';
|
||||
import type { Actions } from './$types';
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, locals: { supabase } }) => {
|
||||
const formData = await request.formData();
|
||||
const password = formData.get('password') as string;
|
||||
const confirmPassword = formData.get('confirmPassword') as string;
|
||||
|
||||
// Validate inputs
|
||||
if (!password || !confirmPassword) {
|
||||
return fail(400, {
|
||||
error: 'Both password fields are required',
|
||||
});
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return fail(400, {
|
||||
error: 'Password must be at least 6 characters long',
|
||||
});
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
return fail(400, {
|
||||
error: 'Passwords do not match',
|
||||
});
|
||||
}
|
||||
|
||||
// Update the user's password
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Password update error:', error);
|
||||
return fail(400, {
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
// Success - redirect to dashboard
|
||||
throw redirect(303, '/dashboard');
|
||||
},
|
||||
};
|
||||
|
|
@ -1,141 +1,103 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Button, Input, Card } from '@manacore/shared-ui';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
let { form } = $props();
|
||||
let loading = $state(false);
|
||||
let hasToken = $state(false);
|
||||
let verifying = $state(true);
|
||||
let verificationError = $state<string | null>(null);
|
||||
let token = $state<string | null>(null);
|
||||
let password = $state('');
|
||||
let confirmPassword = $state('');
|
||||
let error = $state<string | null>(null);
|
||||
let success = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
// Check if we have tokens in the URL hash (from password recovery email)
|
||||
const hash = window.location.hash.substring(1); // Remove the '#'
|
||||
const hashParams = new URLSearchParams(hash);
|
||||
const accessToken = hashParams.get('access_token');
|
||||
const refreshToken = hashParams.get('refresh_token');
|
||||
const type = hashParams.get('type');
|
||||
|
||||
// Check if we have a token in the URL query params (from Supabase email link)
|
||||
const queryToken = $page.url.searchParams.get('token');
|
||||
const queryType = $page.url.searchParams.get('type');
|
||||
|
||||
if (accessToken && refreshToken && type === 'recovery') {
|
||||
// Have tokens in hash - need to establish session
|
||||
try {
|
||||
const response = await fetch('/api/auth/set-session', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
hasToken = true;
|
||||
// Clean up URL by removing hash
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
} else {
|
||||
verificationError = result.error || 'Failed to establish session';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Session establishment error:', error);
|
||||
verificationError = 'Failed to establish session';
|
||||
} finally {
|
||||
verifying = false;
|
||||
}
|
||||
} else if (queryToken && queryType === 'recovery') {
|
||||
// Have token in query params - need to verify via OTP
|
||||
try {
|
||||
const response = await fetch('/api/auth/verify-token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: queryToken, type: queryType }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
hasToken = true;
|
||||
// Clean up URL by removing query params
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
} else {
|
||||
verificationError = result.error || 'Invalid or expired reset link';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token verification error:', error);
|
||||
verificationError = 'Failed to verify reset link';
|
||||
} finally {
|
||||
verifying = false;
|
||||
}
|
||||
} else {
|
||||
// No token found
|
||||
verifying = false;
|
||||
}
|
||||
onMount(() => {
|
||||
// Get token from URL query parameter
|
||||
token = $page.url.searchParams.get('token');
|
||||
hasToken = !!token;
|
||||
});
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = null;
|
||||
|
||||
if (!token) {
|
||||
error = 'Reset token is missing';
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
error = 'Passwords do not match';
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 12) {
|
||||
error = 'Password must be at least 12 characters';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const result = await authStore.resetPassword(token, password);
|
||||
|
||||
if (!result.success) {
|
||||
error = result.error || 'Failed to reset password';
|
||||
} else {
|
||||
success = true;
|
||||
// Redirect to login after 3 seconds
|
||||
setTimeout(() => {
|
||||
goto('/login');
|
||||
}, 3000);
|
||||
}
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'An error occurred';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<h2 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
{#if verifying}
|
||||
Verifying your reset link...
|
||||
{#if success}
|
||||
Password reset successfully
|
||||
{:else if hasToken}
|
||||
Enter your new password
|
||||
{:else}
|
||||
Token missing or expired
|
||||
Invalid or missing reset token
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if verifying}
|
||||
{#if success}
|
||||
<Card class="mt-8">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent"
|
||||
></div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Verifying your password reset link...</p>
|
||||
</div>
|
||||
</Card>
|
||||
{:else if verificationError}
|
||||
<Card class="mt-8">
|
||||
<div class="text-center">
|
||||
<div class="mb-4 text-6xl">⚠️</div>
|
||||
<div class="mb-4 text-6xl">✅</div>
|
||||
<p class="mb-4 text-gray-600 dark:text-gray-400">
|
||||
{verificationError}
|
||||
Your password has been reset successfully. You will be redirected to the login page
|
||||
shortly.
|
||||
</p>
|
||||
<a
|
||||
href="/forgot-password"
|
||||
href="/login"
|
||||
class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400"
|
||||
>
|
||||
Request a new reset link
|
||||
Go to login
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
{:else if hasToken}
|
||||
<Card class="mt-8">
|
||||
<form
|
||||
method="POST"
|
||||
use:enhance={() => {
|
||||
loading = true;
|
||||
return async ({ update }) => {
|
||||
await update();
|
||||
loading = false;
|
||||
};
|
||||
}}
|
||||
>
|
||||
{#if form?.error}
|
||||
<form onsubmit={handleSubmit}>
|
||||
{#if error}
|
||||
<div
|
||||
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
|
||||
>
|
||||
{form.error}
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
@ -152,12 +114,13 @@
|
|||
name="password"
|
||||
id="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="••••••••"
|
||||
placeholder="Enter new password"
|
||||
required
|
||||
minlength={6}
|
||||
minlength={12}
|
||||
bind:value={password}
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Must be at least 6 characters
|
||||
Must be at least 12 characters
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -173,9 +136,10 @@
|
|||
name="confirmPassword"
|
||||
id="confirmPassword"
|
||||
autocomplete="new-password"
|
||||
placeholder="••••••••"
|
||||
placeholder="Confirm new password"
|
||||
required
|
||||
minlength={6}
|
||||
minlength={12}
|
||||
bind:value={confirmPassword}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -195,10 +159,10 @@
|
|||
This password reset link is invalid or has expired.
|
||||
</p>
|
||||
<a
|
||||
href="/login"
|
||||
href="/forgot-password"
|
||||
class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400"
|
||||
>
|
||||
Back to login
|
||||
Request a new reset link
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ cookies }) => {
|
||||
return {
|
||||
cookies: cookies.getAll(),
|
||||
};
|
||||
/**
|
||||
* Server layout load - minimal, auth handled by mana-core-auth client-side
|
||||
*/
|
||||
export const load: LayoutServerLoad = async () => {
|
||||
return {};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,35 +1,14 @@
|
|||
import { waitLocale } from '$lib/i18n';
|
||||
import '$lib/i18n'; // This triggers the init() call at module scope
|
||||
import { createBrowserClient } from '@supabase/ssr';
|
||||
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load: LayoutLoad = async ({ data, depends }) => {
|
||||
/**
|
||||
* Layout load function
|
||||
*
|
||||
* Auth is handled entirely by Mana Core Auth (@manacore/shared-auth).
|
||||
* No Supabase is needed - all data comes from mana-core-auth APIs.
|
||||
*/
|
||||
export const load: LayoutLoad = async () => {
|
||||
await waitLocale();
|
||||
|
||||
/**
|
||||
* Declare a dependency so the layout will be invalidated when `invalidate('supabase:auth')` is called.
|
||||
*/
|
||||
depends('supabase:auth');
|
||||
|
||||
// Create Supabase client for database operations only
|
||||
// Auth is handled by Mana Core Auth (@manacore/shared-auth)
|
||||
const supabase = createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
fetch,
|
||||
},
|
||||
cookies: {
|
||||
getAll() {
|
||||
return data.cookies;
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
// Browser client handles cookies automatically through the browser
|
||||
// This is a no-op as cookies are managed via document.cookie in the browser
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Note: Auth session is managed by Mana Core Auth via authStore,
|
||||
// not Supabase auth. Supabase is used for database operations only.
|
||||
return { supabase };
|
||||
return {};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals: { supabase } }) => {
|
||||
try {
|
||||
const { access_token, refresh_token } = await request.json();
|
||||
|
||||
if (!access_token || !refresh_token) {
|
||||
return json(
|
||||
{ success: false, error: 'Access token and refresh token are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Set the session using the tokens from the URL hash
|
||||
const { data, error } = await supabase.auth.setSession({
|
||||
access_token,
|
||||
refresh_token,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Set session error:', error);
|
||||
return json({ success: false, error: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!data.session) {
|
||||
return json({ success: false, error: 'Failed to create session' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Session is now set via cookies by the Supabase client
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Unexpected error in set session:', error);
|
||||
return json({ success: false, error: 'An unexpected error occurred' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals: { supabase } }) => {
|
||||
try {
|
||||
const { token, type } = await request.json();
|
||||
|
||||
if (!token || type !== 'recovery') {
|
||||
return json({ success: false, error: 'Invalid token or type' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify the OTP token and create a session
|
||||
const { data, error } = await supabase.auth.verifyOtp({
|
||||
token_hash: token,
|
||||
type: 'recovery',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Token verification error:', error);
|
||||
return json({ success: false, error: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!data.session) {
|
||||
return json({ success: false, error: 'Failed to create session' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Session is now set via cookies by the Supabase client
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Unexpected error in token verification:', error);
|
||||
return json({ success: false, error: 'An unexpected error occurred' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import { redirect, fail } from '@sveltejs/kit';
|
||||
import type { Actions } from './$types';
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, locals: { supabase } }) => {
|
||||
const formData = await request.formData();
|
||||
const password = formData.get('password') as string;
|
||||
const confirmPassword = formData.get('confirmPassword') as string;
|
||||
|
||||
// Validate inputs
|
||||
if (!password || !confirmPassword) {
|
||||
return fail(400, {
|
||||
error: 'Both password fields are required',
|
||||
});
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return fail(400, {
|
||||
error: 'Password must be at least 6 characters long',
|
||||
});
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
return fail(400, {
|
||||
error: 'Passwords do not match',
|
||||
});
|
||||
}
|
||||
|
||||
// Update the user's password
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Password update error:', error);
|
||||
return fail(400, {
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
// Success - redirect to dashboard
|
||||
throw redirect(303, '/dashboard');
|
||||
},
|
||||
};
|
||||
|
|
@ -1,214 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Button, Input, Card } from '@manacore/shared-ui';
|
||||
|
||||
let { form, data } = $props();
|
||||
let loading = $state(false);
|
||||
let hasToken = $state(false);
|
||||
let verifying = $state(true);
|
||||
let verificationError = $state<string | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
// Check if we have tokens in the URL hash (from password recovery email)
|
||||
const hash = window.location.hash.substring(1); // Remove the '#'
|
||||
const hashParams = new URLSearchParams(hash);
|
||||
const accessToken = hashParams.get('access_token');
|
||||
const refreshToken = hashParams.get('refresh_token');
|
||||
const type = hashParams.get('type');
|
||||
|
||||
// Check if we have a token in the URL query params (from Supabase email link)
|
||||
const queryToken = $page.url.searchParams.get('token');
|
||||
const queryType = $page.url.searchParams.get('type');
|
||||
|
||||
if (accessToken && refreshToken && type === 'recovery') {
|
||||
// Have tokens in hash - need to establish session
|
||||
try {
|
||||
const response = await fetch('/api/auth/set-session', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
hasToken = true;
|
||||
// Clean up URL by removing hash
|
||||
window.history.replaceState({}, '', '/auth/reset-password');
|
||||
} else {
|
||||
verificationError = result.error || 'Failed to establish session';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Session establishment error:', error);
|
||||
verificationError = 'Failed to establish session';
|
||||
} finally {
|
||||
verifying = false;
|
||||
}
|
||||
} else if (queryToken && queryType === 'recovery') {
|
||||
// Have token in query params - need to verify via OTP
|
||||
try {
|
||||
const response = await fetch('/api/auth/verify-token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: queryToken, type: queryType }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
hasToken = true;
|
||||
// Clean up URL by removing query params
|
||||
window.history.replaceState({}, '', '/auth/reset-password');
|
||||
} else {
|
||||
verificationError = result.error || 'Invalid or expired reset link';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token verification error:', error);
|
||||
verificationError = 'Failed to verify reset link';
|
||||
} finally {
|
||||
verifying = false;
|
||||
}
|
||||
} else {
|
||||
// No token found
|
||||
verifying = false;
|
||||
}
|
||||
// Redirect to the main reset-password page, preserving the token query parameter
|
||||
onMount(() => {
|
||||
const token = $page.url.searchParams.get('token');
|
||||
const redirectUrl = token ? `/reset-password?token=${token}` : '/reset-password';
|
||||
goto(redirectUrl, { replaceState: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Reset Password - ManaCore</title>
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
class="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 dark:bg-gray-900 sm:px-6 lg:px-8"
|
||||
>
|
||||
<div class="w-full max-w-md">
|
||||
<div class="text-center">
|
||||
<h2 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
{#if verifying}
|
||||
Verifying your reset link...
|
||||
{:else if hasToken}
|
||||
Enter your new password
|
||||
{:else}
|
||||
Token missing or expired
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if verifying}
|
||||
<Card class="mt-8">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent"
|
||||
></div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Verifying your password reset link...</p>
|
||||
</div>
|
||||
</Card>
|
||||
{:else if verificationError}
|
||||
<Card class="mt-8">
|
||||
<div class="text-center">
|
||||
<div class="mb-4 text-6xl">⚠️</div>
|
||||
<p class="mb-4 text-gray-600 dark:text-gray-400">
|
||||
{verificationError}
|
||||
</p>
|
||||
<a
|
||||
href="/forgot-password"
|
||||
class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400"
|
||||
>
|
||||
Request a new reset link
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
{:else if hasToken}
|
||||
<Card class="mt-8">
|
||||
<form
|
||||
method="POST"
|
||||
use:enhance={() => {
|
||||
loading = true;
|
||||
return async ({ update }) => {
|
||||
await update();
|
||||
loading = false;
|
||||
};
|
||||
}}
|
||||
>
|
||||
{#if form?.error}
|
||||
<div
|
||||
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
|
||||
>
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
for="password"
|
||||
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
New Password
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
minlength={6}
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Must be at least 6 characters
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="confirmPassword"
|
||||
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
Confirm Password
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
id="confirmPassword"
|
||||
autocomplete="new-password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
minlength={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button type="submit" {loading} class="w-full">
|
||||
{loading ? 'Updating password...' : 'Update password'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
{:else}
|
||||
<Card class="mt-8">
|
||||
<div class="text-center">
|
||||
<div class="mb-4 text-6xl">⚠️</div>
|
||||
<p class="mb-4 text-gray-600 dark:text-gray-400">
|
||||
This password reset link is invalid or has expired.
|
||||
</p>
|
||||
<a
|
||||
href="/login"
|
||||
class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400"
|
||||
>
|
||||
Back to login
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent"
|
||||
></div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Redirecting...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import adapter from '@sveltejs/adapter-netlify';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
|
|
@ -6,7 +6,9 @@ const config = {
|
|||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
adapter: adapter({
|
||||
out: 'build',
|
||||
}),
|
||||
alias: {
|
||||
$lib: 'src/lib',
|
||||
$components: 'src/lib/components',
|
||||
|
|
|
|||
67
apps/todo/apps/backend/Dockerfile
Normal file
67
apps/todo/apps/backend/Dockerfile
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root workspace files
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Copy shared packages
|
||||
COPY packages/shared-errors ./packages/shared-errors
|
||||
COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth
|
||||
|
||||
# Copy todo backend
|
||||
COPY apps/todo/apps/backend ./apps/todo/apps/backend
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build shared packages first
|
||||
WORKDIR /app/packages/shared-errors
|
||||
RUN pnpm build
|
||||
|
||||
WORKDIR /app/packages/shared-nestjs-auth
|
||||
RUN pnpm build
|
||||
|
||||
# Build the backend
|
||||
WORKDIR /app/apps/todo/apps/backend
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Install pnpm and postgresql-client for health checks
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate \
|
||||
&& apk add --no-cache postgresql-client
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy everything from builder (including node_modules)
|
||||
COPY --from=builder /app/pnpm-workspace.yaml ./
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/pnpm-lock.yaml ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/packages ./packages
|
||||
COPY --from=builder /app/apps/todo/apps/backend ./apps/todo/apps/backend
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY apps/todo/apps/backend/docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
WORKDIR /app/apps/todo/apps/backend
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3018
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3018/api/v1/health || exit 1
|
||||
|
||||
# Run entrypoint script
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["node", "dist/main.js"]
|
||||
23
apps/todo/apps/backend/docker-entrypoint.sh
Normal file
23
apps/todo/apps/backend/docker-entrypoint.sh
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "=== Todo Backend Entrypoint ==="
|
||||
|
||||
# Wait for PostgreSQL to be ready
|
||||
echo "Waiting for PostgreSQL..."
|
||||
until pg_isready -h ${DB_HOST:-postgres} -p ${DB_PORT:-5432} -U ${DB_USER:-postgres} 2>/dev/null; do
|
||||
echo "PostgreSQL is unavailable - sleeping"
|
||||
sleep 2
|
||||
done
|
||||
echo "PostgreSQL is up!"
|
||||
|
||||
cd /app/apps/todo/apps/backend
|
||||
|
||||
# Run schema push
|
||||
echo "Pushing database schema..."
|
||||
npx drizzle-kit push --force
|
||||
echo "Schema push completed!"
|
||||
|
||||
# Execute the main command
|
||||
echo "Starting application..."
|
||||
exec "$@"
|
||||
84
apps/todo/apps/web/Dockerfile
Normal file
84
apps/todo/apps/web/Dockerfile
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Build arguments for SvelteKit static env vars
|
||||
ARG PUBLIC_BACKEND_URL=http://todo-backend:3018
|
||||
ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-core-auth:3001
|
||||
|
||||
# Set as environment variables for build
|
||||
ENV PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL
|
||||
ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root workspace files
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Copy shared packages needed by todo web
|
||||
COPY packages/shared-auth ./packages/shared-auth
|
||||
COPY packages/shared-auth-ui ./packages/shared-auth-ui
|
||||
COPY packages/shared-branding ./packages/shared-branding
|
||||
COPY packages/shared-feedback-service ./packages/shared-feedback-service
|
||||
COPY packages/shared-feedback-types ./packages/shared-feedback-types
|
||||
COPY packages/shared-feedback-ui ./packages/shared-feedback-ui
|
||||
COPY packages/shared-i18n ./packages/shared-i18n
|
||||
COPY packages/shared-icons ./packages/shared-icons
|
||||
COPY packages/shared-tailwind ./packages/shared-tailwind
|
||||
COPY packages/shared-theme ./packages/shared-theme
|
||||
COPY packages/shared-theme-ui ./packages/shared-theme-ui
|
||||
COPY packages/shared-subscription-types ./packages/shared-subscription-types
|
||||
COPY packages/shared-subscription-ui ./packages/shared-subscription-ui
|
||||
COPY packages/shared-profile-ui ./packages/shared-profile-ui
|
||||
COPY packages/shared-ui ./packages/shared-ui
|
||||
COPY packages/shared-utils ./packages/shared-utils
|
||||
|
||||
# Copy todo packages and web
|
||||
COPY apps/todo/packages ./apps/todo/packages
|
||||
COPY apps/todo/apps/web ./apps/todo/apps/web
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build shared packages that need building
|
||||
WORKDIR /app/packages/shared-auth
|
||||
RUN pnpm build || true
|
||||
|
||||
# Build the web app
|
||||
WORKDIR /app/apps/todo/apps/web
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Keep same directory structure as builder so pnpm symlinks resolve correctly
|
||||
WORKDIR /app/apps/todo/apps/web
|
||||
|
||||
# Copy the pnpm store that symlinks point to (at /app/node_modules/.pnpm)
|
||||
COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm
|
||||
|
||||
# Copy the app's node_modules (contains symlinks to the pnpm store)
|
||||
COPY --from=builder /app/apps/todo/apps/web/node_modules ./node_modules
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/apps/todo/apps/web/build ./build
|
||||
COPY --from=builder /app/apps/todo/apps/web/package.json ./
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5188
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=5188
|
||||
ENV HOST=0.0.0.0
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:5188/health || exit 1
|
||||
|
||||
# Run the app
|
||||
CMD ["node", "build"]
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"type-check": "echo 'Skipping type-check for now'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-node": "^5.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
|
|
|
|||
27
apps/todo/apps/web/src/hooks.server.ts
Normal file
27
apps/todo/apps/web/src/hooks.server.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Server Hooks for SvelteKit
|
||||
* - Injects runtime environment variables for client-side use
|
||||
* - Auth is handled client-side via Mana Core Auth
|
||||
*/
|
||||
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
// Get client-side URLs from environment (Docker runtime)
|
||||
const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
|
||||
process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || '';
|
||||
const PUBLIC_BACKEND_URL_CLIENT =
|
||||
process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || '';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) => {
|
||||
// Inject runtime environment variables into the HTML
|
||||
// These will be available on window.__PUBLIC_*__ for client-side code
|
||||
const envScript = `<script>
|
||||
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${PUBLIC_MANA_CORE_AUTH_URL_CLIENT}";
|
||||
window.__PUBLIC_BACKEND_URL__ = "${PUBLIC_BACKEND_URL_CLIENT}";
|
||||
</script>`;
|
||||
return html.replace('<head>', `<head>${envScript}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -12,12 +12,28 @@ interface ApiError {
|
|||
statusCode: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the backend URL, preferring runtime-injected value in browser
|
||||
* This allows Docker to inject PUBLIC_BACKEND_URL_CLIENT at runtime
|
||||
* instead of using the build-time PUBLIC_BACKEND_URL
|
||||
*/
|
||||
function getBackendUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const runtimeUrl = (window as Window & { __PUBLIC_BACKEND_URL__?: string })
|
||||
.__PUBLIC_BACKEND_URL__;
|
||||
if (runtimeUrl) {
|
||||
return runtimeUrl;
|
||||
}
|
||||
}
|
||||
return PUBLIC_BACKEND_URL || 'http://localhost:3018';
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
private baseUrl: string;
|
||||
private accessToken: string | null = null;
|
||||
|
||||
constructor() {
|
||||
this.baseUrl = PUBLIC_BACKEND_URL || 'http://localhost:3018';
|
||||
// Use getter to evaluate URL at request time (browser may hydrate after construction)
|
||||
private get baseUrl(): string {
|
||||
return getBackendUrl();
|
||||
}
|
||||
|
||||
setAccessToken(token: string | null) {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,18 @@ import { browser } from '$app/environment';
|
|||
import { initializeWebAuth, type UserData } from '@manacore/shared-auth';
|
||||
import { apiClient } from '$lib/api/client';
|
||||
|
||||
// Initialize Mana Core Auth only on the client side
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime - fallback for SSR and client
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
// Client-side: use injected window variable (set by hooks.server.ts)
|
||||
// Falls back to localhost for local development
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
// Server-side (SSR): use Docker internal URL for container-to-container communication
|
||||
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Lazy initialization to avoid SSR issues with localStorage
|
||||
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
|
||||
|
|
@ -17,7 +27,7 @@ let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null =
|
|||
function getAuthService() {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
|
||||
const auth = initializeWebAuth({ baseUrl: getAuthUrl() });
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,19 @@
|
|||
import { browser } from '$app/environment';
|
||||
import { createUserSettingsStore } from '@manacore/shared-theme';
|
||||
import { authStore } from './auth.svelte';
|
||||
|
||||
const MANA_AUTH_URL = import.meta.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
// Get auth URL dynamically at runtime
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const userSettings = createUserSettingsStore({
|
||||
appId: 'todo',
|
||||
authUrl: MANA_AUTH_URL,
|
||||
authUrl: getAuthUrl(),
|
||||
getAccessToken: () => authStore.getAccessToken(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import adapter from '@sveltejs/adapter-auto';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
adapter: adapter({
|
||||
out: 'build',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -74,8 +74,8 @@ services:
|
|||
JWT_SECRET: ${JWT_SECRET}
|
||||
JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY}
|
||||
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
|
||||
# CORS - Allow chat-web and other staging origins
|
||||
CORS_ORIGINS: http://46.224.108.214:3000,http://46.224.108.214:3002,http://localhost:3000
|
||||
# CORS - Allow all staging web app origins
|
||||
CORS_ORIGINS: http://46.224.108.214:3000,http://46.224.108.214:5173,http://46.224.108.214:5186,http://46.224.108.214:5187,http://46.224.108.214:5188,http://localhost:3000,http://localhost:5173,http://localhost:5186,http://localhost:5187,http://localhost:5188
|
||||
ports:
|
||||
- "3001:3001"
|
||||
healthcheck:
|
||||
|
|
@ -159,6 +159,250 @@ services:
|
|||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# ============================================
|
||||
# Manacore App
|
||||
# ============================================
|
||||
|
||||
manacore-web:
|
||||
image: ${DOCKER_REGISTRY:-ghcr.io/memo-2023}/manacore-web:${MANACORE_WEB_VERSION:-latest}
|
||||
container_name: manacore-web-staging
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
mana-core-auth:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: staging
|
||||
PORT: 5173
|
||||
# Auth URLs
|
||||
PUBLIC_MANA_CORE_AUTH_URL: http://mana-core-auth:3001
|
||||
PUBLIC_MANA_CORE_AUTH_URL_CLIENT: http://46.224.108.214:3001
|
||||
# Backend URLs for dashboard widgets
|
||||
PUBLIC_TODO_API_URL: http://todo-backend:3018
|
||||
PUBLIC_TODO_API_URL_CLIENT: http://46.224.108.214:3018
|
||||
PUBLIC_CALENDAR_API_URL: http://calendar-backend:3016
|
||||
PUBLIC_CALENDAR_API_URL_CLIENT: http://46.224.108.214:3016
|
||||
PUBLIC_CLOCK_API_URL: http://clock-backend:3017
|
||||
PUBLIC_CLOCK_API_URL_CLIENT: http://46.224.108.214:3017
|
||||
ports:
|
||||
- "5173:5173"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5173/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- manacore-network
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# ============================================
|
||||
# Todo App
|
||||
# ============================================
|
||||
|
||||
todo-backend:
|
||||
image: ${DOCKER_REGISTRY:-ghcr.io/memo-2023}/todo-backend:${TODO_VERSION:-latest}
|
||||
container_name: todo-backend-staging
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
mana-core-auth:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: staging
|
||||
PORT: 3018
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@postgres:5432/todo
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_USER: ${POSTGRES_USER:-postgres}
|
||||
MANA_CORE_AUTH_URL: http://mana-core-auth:3001
|
||||
CORS_ORIGINS: http://46.224.108.214:5188,http://46.224.108.214:5173,http://localhost:5188,http://localhost:5173
|
||||
ports:
|
||||
- "3018:3018"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3018/api/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- manacore-network
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
todo-web:
|
||||
image: ${DOCKER_REGISTRY:-ghcr.io/memo-2023}/todo-web:${TODO_WEB_VERSION:-latest}
|
||||
container_name: todo-web-staging
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
todo-backend:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: staging
|
||||
PORT: 5188
|
||||
PUBLIC_BACKEND_URL: http://todo-backend:3018
|
||||
PUBLIC_MANA_CORE_AUTH_URL: http://mana-core-auth:3001
|
||||
PUBLIC_BACKEND_URL_CLIENT: http://46.224.108.214:3018
|
||||
PUBLIC_MANA_CORE_AUTH_URL_CLIENT: http://46.224.108.214:3001
|
||||
ports:
|
||||
- "5188:5188"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5188/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- manacore-network
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# ============================================
|
||||
# Calendar App
|
||||
# ============================================
|
||||
|
||||
calendar-backend:
|
||||
image: ${DOCKER_REGISTRY:-ghcr.io/memo-2023}/calendar-backend:${CALENDAR_VERSION:-latest}
|
||||
container_name: calendar-backend-staging
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
mana-core-auth:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: staging
|
||||
PORT: 3016
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@postgres:5432/calendar
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_USER: ${POSTGRES_USER:-postgres}
|
||||
MANA_CORE_AUTH_URL: http://mana-core-auth:3001
|
||||
CORS_ORIGINS: http://46.224.108.214:5186,http://46.224.108.214:5173,http://localhost:5186,http://localhost:5173
|
||||
ports:
|
||||
- "3016:3016"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3016/api/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- manacore-network
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
calendar-web:
|
||||
image: ${DOCKER_REGISTRY:-ghcr.io/memo-2023}/calendar-web:${CALENDAR_WEB_VERSION:-latest}
|
||||
container_name: calendar-web-staging
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
calendar-backend:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: staging
|
||||
PORT: 5186
|
||||
PUBLIC_BACKEND_URL: http://calendar-backend:3016
|
||||
PUBLIC_MANA_CORE_AUTH_URL: http://mana-core-auth:3001
|
||||
PUBLIC_BACKEND_URL_CLIENT: http://46.224.108.214:3016
|
||||
PUBLIC_MANA_CORE_AUTH_URL_CLIENT: http://46.224.108.214:3001
|
||||
ports:
|
||||
- "5186:5186"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5186/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- manacore-network
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# ============================================
|
||||
# Clock App
|
||||
# ============================================
|
||||
|
||||
clock-backend:
|
||||
image: ${DOCKER_REGISTRY:-ghcr.io/memo-2023}/clock-backend:${CLOCK_VERSION:-latest}
|
||||
container_name: clock-backend-staging
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
mana-core-auth:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: staging
|
||||
PORT: 3017
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@postgres:5432/clock
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_USER: ${POSTGRES_USER:-postgres}
|
||||
MANA_CORE_AUTH_URL: http://mana-core-auth:3001
|
||||
CORS_ORIGINS: http://46.224.108.214:5187,http://46.224.108.214:5173,http://localhost:5187,http://localhost:5173
|
||||
ports:
|
||||
- "3017:3017"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3017/api/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- manacore-network
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
clock-web:
|
||||
image: ${DOCKER_REGISTRY:-ghcr.io/memo-2023}/clock-web:${CLOCK_WEB_VERSION:-latest}
|
||||
container_name: clock-web-staging
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
clock-backend:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: staging
|
||||
PORT: 5187
|
||||
PUBLIC_BACKEND_URL: http://clock-backend:3017
|
||||
PUBLIC_MANA_CORE_AUTH_URL: http://mana-core-auth:3001
|
||||
PUBLIC_BACKEND_URL_CLIENT: http://46.224.108.214:3017
|
||||
PUBLIC_MANA_CORE_AUTH_URL_CLIENT: http://46.224.108.214:3001
|
||||
ports:
|
||||
- "5187:5187"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5187/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- manacore-network
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# ============================================
|
||||
# Networks
|
||||
# ============================================
|
||||
|
|
|
|||
|
|
@ -1,21 +1,37 @@
|
|||
-- Create databases for all services
|
||||
-- This script runs on first container initialization
|
||||
|
||||
-- Create chat database
|
||||
CREATE DATABASE chat;
|
||||
|
||||
-- Create voxel_lava database
|
||||
CREATE DATABASE voxel_lava;
|
||||
|
||||
-- Create storage database (cloud drive)
|
||||
CREATE DATABASE storage;
|
||||
|
||||
-- Create todo database
|
||||
CREATE DATABASE todo;
|
||||
-- Core databases
|
||||
CREATE DATABASE IF NOT EXISTS chat;
|
||||
CREATE DATABASE IF NOT EXISTS zitare;
|
||||
CREATE DATABASE IF NOT EXISTS contacts;
|
||||
CREATE DATABASE IF NOT EXISTS calendar;
|
||||
CREATE DATABASE IF NOT EXISTS clock;
|
||||
CREATE DATABASE IF NOT EXISTS todo;
|
||||
CREATE DATABASE IF NOT EXISTS manadeck;
|
||||
CREATE DATABASE IF NOT EXISTS storage;
|
||||
CREATE DATABASE IF NOT EXISTS mail;
|
||||
CREATE DATABASE IF NOT EXISTS moodlit;
|
||||
CREATE DATABASE IF NOT EXISTS finance;
|
||||
CREATE DATABASE IF NOT EXISTS inventory;
|
||||
CREATE DATABASE IF NOT EXISTS techbase;
|
||||
CREATE DATABASE IF NOT EXISTS voxel_lava;
|
||||
CREATE DATABASE IF NOT EXISTS figgos;
|
||||
|
||||
-- Grant all privileges to the default user
|
||||
GRANT ALL PRIVILEGES ON DATABASE chat TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE voxel_lava TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE manacore TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE storage TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE zitare TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE contacts TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE calendar TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE clock TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE todo TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE manadeck TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE storage TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE mail TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE moodlit TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE finance TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE inventory TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE techbase TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE voxel_lava TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE figgos TO manacore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE manacore TO manacore;
|
||||
|
|
|
|||
667
docs/DATABASE_MIGRATIONS.md
Normal file
667
docs/DATABASE_MIGRATIONS.md
Normal file
|
|
@ -0,0 +1,667 @@
|
|||
# Database Migration Guide
|
||||
|
||||
This document describes database migration best practices, procedures, and tooling for the ManaCore monorepo. **This is a core system concept** - all developers should understand these patterns.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Drizzle Migration Internals](#drizzle-migration-internals)
|
||||
3. [Migration Commands](#migration-commands)
|
||||
4. [Development vs Production](#development-vs-production)
|
||||
5. [CI/CD Pipeline](#cicd-pipeline)
|
||||
6. [Advisory Locks](#advisory-locks)
|
||||
7. [Zero-Downtime Migrations](#zero-downtime-migrations)
|
||||
8. [Rollback Procedures](#rollback-procedures)
|
||||
9. [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
All backends in the ManaCore monorepo use **Drizzle ORM** for database schema management. We use two different approaches depending on the environment:
|
||||
|
||||
| Environment | Command | Purpose |
|
||||
|-------------|---------|---------|
|
||||
| **Development** | `drizzle-kit push` | Fast iteration, direct schema sync |
|
||||
| **Production** | `drizzle-kit generate` + `migrate` | Tracked migrations with history |
|
||||
|
||||
### Key Principles
|
||||
|
||||
1. **Migrations run BEFORE code deployment** - Ensures database is ready for new code
|
||||
2. **Advisory locks prevent concurrent migrations** - Safe for multi-replica deployments
|
||||
3. **Expand-contract pattern for breaking changes** - Zero-downtime schema changes
|
||||
4. **Data persistence** - Migrations never delete user data unless explicitly requested
|
||||
|
||||
### Quick Decision Guide
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Which command should I use? │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Local development? │
|
||||
│ └── YES → pnpm db:push (fast, no tracking) │
|
||||
│ │
|
||||
│ Staging/Production? │
|
||||
│ └── YES → pnpm db:generate + pnpm db:migrate (tracked) │
|
||||
│ │
|
||||
│ Need to inspect data? │
|
||||
│ └── YES → pnpm db:studio (opens Drizzle Studio) │
|
||||
│ │
|
||||
│ Schema changed by someone else? │
|
||||
│ └── YES → git pull + pnpm db:push (local) │
|
||||
│ git pull + pnpm db:migrate (staging/prod) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Drizzle Migration Internals
|
||||
|
||||
Understanding how Drizzle manages migrations is essential for debugging issues.
|
||||
|
||||
### The Two Modes
|
||||
|
||||
#### 1. Push Mode (`drizzle-kit push`)
|
||||
|
||||
**How it works:**
|
||||
1. Drizzle introspects your TypeScript schema files
|
||||
2. Drizzle introspects the current database schema
|
||||
3. Drizzle computes the diff between them
|
||||
4. Drizzle generates and **immediately executes** the SQL to sync them
|
||||
|
||||
**Characteristics:**
|
||||
- No migration files created
|
||||
- No history tracking
|
||||
- Direct database modification
|
||||
- Interactive confirmation (use `--force` to skip)
|
||||
|
||||
**When to use:** Local development, experimentation, prototyping
|
||||
|
||||
#### 2. Generate + Migrate Mode (`drizzle-kit generate` + `migrate`)
|
||||
|
||||
**How it works:**
|
||||
|
||||
**Step 1: Generate** (`drizzle-kit generate`)
|
||||
1. Drizzle introspects your TypeScript schema files
|
||||
2. Drizzle reads the last snapshot from `migrations/meta/`
|
||||
3. Drizzle computes the diff
|
||||
4. Drizzle creates migration files (SQL + snapshot)
|
||||
|
||||
**Step 2: Migrate** (`pnpm db:migrate`)
|
||||
1. Script reads `migrations/meta/_journal.json`
|
||||
2. Script queries `__drizzle_migrations` table in database
|
||||
3. Script determines which migrations haven't been applied
|
||||
4. Script executes pending migrations in order
|
||||
5. Script records applied migrations in `__drizzle_migrations`
|
||||
|
||||
**Characteristics:**
|
||||
- Creates versioned SQL files
|
||||
- Full history tracking
|
||||
- Repeatable deployments
|
||||
- Can be reviewed before applying
|
||||
|
||||
**When to use:** Staging, production, CI/CD pipelines
|
||||
|
||||
### Migration File Structure
|
||||
|
||||
```
|
||||
src/db/migrations/
|
||||
├── 0000_initial_schema/
|
||||
│ ├── migration.sql # The actual SQL to execute
|
||||
│ └── snapshot.json # Schema snapshot AFTER this migration
|
||||
├── 0001_add_user_preferences/
|
||||
│ ├── migration.sql
|
||||
│ └── snapshot.json
|
||||
├── 0002_add_credits_table/
|
||||
│ ├── migration.sql
|
||||
│ └── snapshot.json
|
||||
└── meta/
|
||||
└── _journal.json # Migration registry (order + metadata)
|
||||
```
|
||||
|
||||
### The Journal File (`_journal.json`)
|
||||
|
||||
This file tracks all generated migrations:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1733066521000,
|
||||
"tag": "0000_initial_schema",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1733152921000,
|
||||
"tag": "0001_add_user_preferences",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Key fields:**
|
||||
- `idx`: Sequential index (order matters!)
|
||||
- `tag`: Folder name containing the migration
|
||||
- `when`: Unix timestamp when generated
|
||||
- `breakpoints`: Whether to use statement breakpoints
|
||||
|
||||
### The Database Tracking Table (`__drizzle_migrations`)
|
||||
|
||||
Drizzle creates this table automatically to track applied migrations:
|
||||
|
||||
```sql
|
||||
-- Schema: drizzle
|
||||
-- Table: __drizzle_migrations
|
||||
CREATE TABLE drizzle.__drizzle_migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
hash TEXT NOT NULL,
|
||||
created_at BIGINT NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
**Query applied migrations:**
|
||||
```sql
|
||||
SELECT * FROM drizzle.__drizzle_migrations ORDER BY created_at;
|
||||
```
|
||||
|
||||
### How Migration Tracking Works
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ _journal.json │ │ __drizzle_ │
|
||||
│ (filesystem) │ │ migrations (db) │
|
||||
└────────┬────────┘ └────────┬────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
[0000, 0001, 0002] [hash_0000, hash_0001]
|
||||
│ │
|
||||
└───────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
Pending: [0002]
|
||||
│
|
||||
▼
|
||||
Execute 0002/migration.sql
|
||||
│
|
||||
▼
|
||||
Insert into __drizzle_migrations
|
||||
```
|
||||
|
||||
### Snapshot Files
|
||||
|
||||
Each migration includes a `snapshot.json` that captures the **complete schema state** after that migration. This allows Drizzle to:
|
||||
|
||||
1. Compute diffs for the next migration
|
||||
2. Detect schema drift
|
||||
3. Generate accurate SQL
|
||||
|
||||
**Important:** Never modify snapshots manually!
|
||||
|
||||
---
|
||||
|
||||
## Migration Commands
|
||||
|
||||
### All Backends
|
||||
|
||||
```bash
|
||||
# Development - push schema directly (fast, no history)
|
||||
pnpm db:push
|
||||
|
||||
# Generate migration files from schema changes
|
||||
pnpm db:generate
|
||||
|
||||
# Run migrations with advisory locks (production-safe)
|
||||
pnpm db:migrate
|
||||
|
||||
# Open Drizzle Studio for database inspection
|
||||
pnpm db:studio
|
||||
```
|
||||
|
||||
### Root-Level Commands
|
||||
|
||||
```bash
|
||||
# Setup all databases (creates DBs + pushes schemas)
|
||||
pnpm setup:db
|
||||
|
||||
# Setup specific service
|
||||
pnpm setup:db:auth
|
||||
pnpm setup:db:chat
|
||||
```
|
||||
|
||||
### Per-Service Commands
|
||||
|
||||
```bash
|
||||
# mana-core-auth
|
||||
pnpm --filter mana-core-auth db:push
|
||||
pnpm --filter mana-core-auth db:generate
|
||||
pnpm --filter mana-core-auth db:migrate
|
||||
|
||||
# chat-backend
|
||||
pnpm --filter @chat/backend db:push
|
||||
pnpm --filter @chat/backend db:migrate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development vs Production
|
||||
|
||||
### Development Workflow
|
||||
|
||||
For local development, use `db:push` for fast iteration:
|
||||
|
||||
```bash
|
||||
# 1. Make schema changes in src/db/schema/*.ts
|
||||
# 2. Push changes to local database
|
||||
pnpm db:push
|
||||
|
||||
# Or use the full dev command which handles this automatically
|
||||
pnpm dev:chat:full
|
||||
```
|
||||
|
||||
**Why `push` for development?**
|
||||
- Instant feedback on schema changes
|
||||
- No migration file clutter during experimentation
|
||||
- Automatically handled by `dev:*:full` commands
|
||||
|
||||
### Production Workflow
|
||||
|
||||
For staging/production, use migration files for trackability:
|
||||
|
||||
```bash
|
||||
# 1. Make schema changes in src/db/schema/*.ts
|
||||
|
||||
# 2. Generate migration file
|
||||
pnpm db:generate --name add_user_preferences
|
||||
|
||||
# 3. Review generated SQL
|
||||
cat src/db/migrations/*/migration.sql
|
||||
|
||||
# 4. Commit migration files
|
||||
git add src/db/migrations/
|
||||
git commit -m "feat: add user preferences table"
|
||||
|
||||
# 5. CI/CD runs migrations automatically on deploy
|
||||
```
|
||||
|
||||
**Why migrations for production?**
|
||||
- Audit trail of all schema changes
|
||||
- Repeatable deployments
|
||||
- Rollback capability (with manual down migrations)
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Pipeline
|
||||
|
||||
### Deployment Flow
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Build │───>│ Create DB │───>│ Migrate │───>│ Deploy │
|
||||
│ Images │ │ (if new) │ │ Database │ │ Code │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
### Migration Step Features
|
||||
|
||||
1. **Retry logic** - 3 attempts with exponential backoff (10s, 20s, 30s)
|
||||
2. **Timeout protection** - 5-minute timeout per migration
|
||||
3. **Advisory locks** - Prevents concurrent migrations
|
||||
4. **Graceful fallback** - Falls back to `db:push` if `db:migrate` unavailable
|
||||
|
||||
### Staging Deployment
|
||||
|
||||
Migrations run automatically after database creation:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/cd-staging.yml
|
||||
- name: Run database migrations
|
||||
run: |
|
||||
docker compose exec -T mana-core-auth pnpm run db:migrate
|
||||
```
|
||||
|
||||
### Production Deployment
|
||||
|
||||
Migrations run BEFORE deploying new code:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/cd-production.yml
|
||||
- name: Run database migrations
|
||||
run: |
|
||||
docker compose run --rm mana-core-auth pnpm run db:migrate
|
||||
|
||||
- name: Deploy with zero-downtime
|
||||
run: |
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advisory Locks
|
||||
|
||||
Advisory locks prevent multiple instances from running migrations simultaneously.
|
||||
|
||||
### How It Works
|
||||
|
||||
```typescript
|
||||
// services/mana-core-auth/src/db/migrate.ts
|
||||
|
||||
const MIGRATION_LOCK_ID = 987654321;
|
||||
|
||||
// Acquire lock before migration
|
||||
await db.execute(sql`SELECT pg_try_advisory_lock(${LOCK_ID})`);
|
||||
|
||||
// Run migrations...
|
||||
|
||||
// Release lock after migration
|
||||
await db.execute(sql`SELECT pg_advisory_unlock(${LOCK_ID})`);
|
||||
```
|
||||
|
||||
### Lock Behavior
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| Lock acquired | Migration runs immediately |
|
||||
| Lock held by another process | Waits up to 5 minutes, then fails |
|
||||
| Lock stuck | Manual release required (see Troubleshooting) |
|
||||
|
||||
### Lock IDs by Service
|
||||
|
||||
| Service | Lock ID |
|
||||
|---------|---------|
|
||||
| mana-core-auth | `987654321` |
|
||||
| chat-backend | (to be assigned) |
|
||||
| todo-backend | (to be assigned) |
|
||||
|
||||
### Migration Script Architecture
|
||||
|
||||
The production migration script (`src/db/migrate.ts`) is designed for safe, concurrent-safe deployments:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ migrate.ts Execution Flow │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. Load environment variables (.env) │
|
||||
│ └── DATABASE_URL, MIGRATION_TIMEOUT │
|
||||
│ │
|
||||
│ 2. Create single-connection pool │
|
||||
│ └── max: 1 (dedicated migration connection) │
|
||||
│ │
|
||||
│ 3. Test database connectivity (with retry) │
|
||||
│ └── SELECT 1 (max 3 attempts, exponential backoff) │
|
||||
│ │
|
||||
│ 4. Acquire advisory lock │
|
||||
│ ├── pg_try_advisory_lock() - non-blocking attempt │
|
||||
│ └── If busy: poll every 5s until timeout (default: 5 min) │
|
||||
│ │
|
||||
│ 5. Check for migration files │
|
||||
│ └── If meta/_journal.json missing: exit gracefully │
|
||||
│ │
|
||||
│ 6. Run Drizzle migrations │
|
||||
│ └── migrate(db, { migrationsFolder }) │
|
||||
│ │
|
||||
│ 7. Cleanup (always runs, even on error) │
|
||||
│ ├── Release advisory lock │
|
||||
│ └── Close database connection │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key Components:**
|
||||
|
||||
| Component | Purpose | Configuration |
|
||||
|-----------|---------|---------------|
|
||||
| `withRetry()` | Retry transient errors (network, connection) | 3 attempts, exponential backoff |
|
||||
| `acquireLock()` | Non-blocking lock attempt | `pg_try_advisory_lock()` |
|
||||
| `waitForLock()` | Polling wait for lock | 5s intervals, configurable timeout |
|
||||
| `releaseLock()` | Release lock in finally block | Always runs |
|
||||
|
||||
**Error Handling:**
|
||||
|
||||
```typescript
|
||||
// Transient errors (will retry):
|
||||
- ECONNREFUSED, ETIMEDOUT, ENOTFOUND
|
||||
- Connection errors
|
||||
- PostgreSQL 57P03 (cannot connect now)
|
||||
|
||||
// Non-transient errors (immediate failure):
|
||||
- Missing DATABASE_URL
|
||||
- SQL syntax errors
|
||||
- Schema conflicts
|
||||
- Lock timeout
|
||||
```
|
||||
|
||||
**Exit Codes:**
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 0 | Success - all migrations applied |
|
||||
| 1 | Failure - check logs for details |
|
||||
|
||||
---
|
||||
|
||||
## Zero-Downtime Migrations
|
||||
|
||||
For breaking schema changes, use the **expand-contract pattern**:
|
||||
|
||||
### Phase 1: Expand
|
||||
|
||||
Add new schema elements alongside existing ones:
|
||||
|
||||
```sql
|
||||
-- Migration: 001_add_full_name.sql
|
||||
ALTER TABLE users ADD COLUMN full_name TEXT;
|
||||
```
|
||||
|
||||
### Phase 2: Migrate
|
||||
|
||||
Update application to write to both, backfill data:
|
||||
|
||||
```typescript
|
||||
// Application code - dual write
|
||||
await db.update(users).set({
|
||||
name: newName, // Old column
|
||||
fullName: newName, // New column
|
||||
});
|
||||
|
||||
// Backfill script
|
||||
UPDATE users SET full_name = name WHERE full_name IS NULL;
|
||||
```
|
||||
|
||||
### Phase 3: Contract
|
||||
|
||||
After 1-2 weeks, remove old column:
|
||||
|
||||
```sql
|
||||
-- Migration: 002_drop_name_column.sql
|
||||
ALTER TABLE users DROP COLUMN name;
|
||||
```
|
||||
|
||||
### Common Patterns
|
||||
|
||||
| Change Type | Approach |
|
||||
|-------------|----------|
|
||||
| Add column | Direct `ALTER TABLE ADD COLUMN` |
|
||||
| Drop column | Remove from code first, wait 2 weeks, then drop |
|
||||
| Rename column | Add new → dual-write → backfill → drop old |
|
||||
| Change type | Add new column → backfill with cast → swap |
|
||||
| Add NOT NULL | Add nullable → backfill → add constraint |
|
||||
|
||||
### Index Creation
|
||||
|
||||
Always use `CONCURRENTLY` to avoid table locks:
|
||||
|
||||
```sql
|
||||
-- Good
|
||||
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
|
||||
|
||||
-- Bad (locks table)
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rollback Procedures
|
||||
|
||||
### Automatic Rollback (Not Supported)
|
||||
|
||||
Drizzle ORM does not support automatic rollbacks. Plan your migrations carefully.
|
||||
|
||||
### Manual Rollback
|
||||
|
||||
1. **Write down migration scripts** alongside up migrations:
|
||||
|
||||
```
|
||||
src/db/migrations/
|
||||
├── 001_add_referrals.up.sql
|
||||
├── 001_add_referrals.down.sql # Manual rollback script
|
||||
```
|
||||
|
||||
2. **Execute rollback manually**:
|
||||
|
||||
```bash
|
||||
# Connect to database
|
||||
docker compose exec -T postgres psql -U postgres -d manacore_auth
|
||||
|
||||
# Run down migration
|
||||
\i /path/to/001_add_referrals.down.sql
|
||||
```
|
||||
|
||||
### Rollback Checklist
|
||||
|
||||
- [ ] Identify affected migration
|
||||
- [ ] Verify rollback script exists and is tested
|
||||
- [ ] Create database backup before rollback
|
||||
- [ ] Execute rollback in staging first
|
||||
- [ ] Monitor for issues after rollback
|
||||
- [ ] Update application code if needed
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Migration Lock Stuck
|
||||
|
||||
If a migration lock is stuck (process crashed without releasing):
|
||||
|
||||
```sql
|
||||
-- Check for stuck locks
|
||||
SELECT * FROM pg_locks WHERE locktype = 'advisory';
|
||||
|
||||
-- Release specific lock (replace LOCK_ID)
|
||||
SELECT pg_advisory_unlock(987654321);
|
||||
|
||||
-- Release all advisory locks for current session
|
||||
SELECT pg_advisory_unlock_all();
|
||||
```
|
||||
|
||||
### Migration Timeout
|
||||
|
||||
If migrations time out:
|
||||
|
||||
1. Check for long-running queries: `SELECT * FROM pg_stat_activity;`
|
||||
2. Increase timeout: `MIGRATION_TIMEOUT=600 pnpm db:migrate`
|
||||
3. Break large migrations into smaller steps
|
||||
|
||||
### Schema Drift
|
||||
|
||||
If staging/production schema differs from expected:
|
||||
|
||||
```bash
|
||||
# Generate migration from current schema
|
||||
pnpm db:generate --name sync_schema
|
||||
|
||||
# Review and apply
|
||||
pnpm db:migrate
|
||||
```
|
||||
|
||||
### Connection Issues
|
||||
|
||||
```bash
|
||||
# Test database connectivity
|
||||
docker compose exec -T postgres pg_isready -U postgres
|
||||
|
||||
# Check environment variables
|
||||
echo $DATABASE_URL
|
||||
|
||||
# Manual connection test
|
||||
docker compose exec -T postgres psql -U postgres -d manacore_auth -c "SELECT 1"
|
||||
```
|
||||
|
||||
### Migration Fails in CI/CD
|
||||
|
||||
1. Check GitHub Actions logs for specific error
|
||||
2. Verify DATABASE_URL is correctly set in secrets
|
||||
3. Ensure database exists before migration runs
|
||||
4. Check if another migration is running (advisory lock)
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Run migrations before deploying new code
|
||||
- Test migrations in staging before production
|
||||
- Use `CONCURRENTLY` for index creation
|
||||
- Keep migrations small and focused
|
||||
- Commit migration files to version control
|
||||
- Wait 1-2 weeks before dropping columns
|
||||
|
||||
### DON'T
|
||||
|
||||
- Run `db:push` in production
|
||||
- Delete migration files after they've been applied
|
||||
- Modify migration files after they've been applied
|
||||
- Add NOT NULL without default or backfill
|
||||
- Create indexes without `CONCURRENTLY`
|
||||
- Drop columns immediately after removing from code
|
||||
|
||||
---
|
||||
|
||||
## Migration File Structure
|
||||
|
||||
```
|
||||
services/mana-core-auth/
|
||||
├── src/db/
|
||||
│ ├── schema/
|
||||
│ │ ├── index.ts # Export all schemas
|
||||
│ │ ├── auth.schema.ts # User, session tables
|
||||
│ │ └── credits.schema.ts # Credit system tables
|
||||
│ ├── migrations/
|
||||
│ │ ├── 0001_initial/
|
||||
│ │ │ ├── snapshot.json
|
||||
│ │ │ └── migration.sql
|
||||
│ │ └── meta/
|
||||
│ │ └── _journal.json # Migration history
|
||||
│ ├── connection.ts # Database connection
|
||||
│ └── migrate.ts # Migration script with locks
|
||||
└── drizzle.config.ts # Drizzle configuration
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `DATABASE_URL` | PostgreSQL connection string | Required |
|
||||
| `MIGRATION_TIMEOUT` | Max seconds for migration | `300` |
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Drizzle ORM Migrations](https://orm.drizzle.team/docs/migrations)
|
||||
- [PostgreSQL Advisory Locks](https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS)
|
||||
- [Expand-Contract Pattern](https://martinfowler.com/bliki/ParallelChange.html)
|
||||
- [Zero-Downtime PostgreSQL Migrations](https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries)
|
||||
275
docs/LOCAL_DEVELOPMENT.md
Normal file
275
docs/LOCAL_DEVELOPMENT.md
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
# Local Development Guide
|
||||
|
||||
This guide explains how to set up and run applications locally with automatic database setup.
|
||||
|
||||
## Quick Start
|
||||
|
||||
For any project with a backend, use the `dev:*:full` command:
|
||||
|
||||
```bash
|
||||
pnpm dev:chat:full # Start chat with auth + database setup
|
||||
pnpm dev:zitare:full # Start zitare with auth + database setup
|
||||
pnpm dev:contacts:full # Start contacts with auth + database setup
|
||||
# ... etc
|
||||
```
|
||||
|
||||
These commands automatically:
|
||||
1. Create the database if it doesn't exist
|
||||
2. Push the latest schema (Drizzle `db:push`)
|
||||
3. Start the auth service (mana-core-auth)
|
||||
4. Start the backend and web app with colored output
|
||||
|
||||
## Available Full Dev Commands
|
||||
|
||||
| Command | Database | Backend Port | Web Port |
|
||||
|---------|----------|--------------|----------|
|
||||
| `pnpm dev:chat:full` | chat | 3002 | 5173 |
|
||||
| `pnpm dev:zitare:full` | zitare | 3007 | 5177 |
|
||||
| `pnpm dev:contacts:full` | contacts | 3015 | 5184 |
|
||||
| `pnpm dev:calendar:full` | calendar | 3014 | 5179 |
|
||||
| `pnpm dev:clock:full` | clock | 3017 | 5187 |
|
||||
| `pnpm dev:todo:full` | todo | 3018 | 5188 |
|
||||
| `pnpm dev:picture:full` | picture | 3006 | 5175 |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before running any `dev:*:full` command:
|
||||
|
||||
```bash
|
||||
# 1. Start Docker infrastructure (PostgreSQL, Redis, MinIO)
|
||||
pnpm docker:up
|
||||
|
||||
# 2. Generate environment files (runs automatically on pnpm install)
|
||||
pnpm setup:env
|
||||
```
|
||||
|
||||
## Database Setup Commands
|
||||
|
||||
### Individual Service Setup
|
||||
|
||||
```bash
|
||||
pnpm setup:db:auth # Setup mana-core-auth database + schema
|
||||
pnpm setup:db:chat # Setup chat database + schema
|
||||
pnpm setup:db:zitare # Setup zitare database + schema
|
||||
pnpm setup:db:contacts # Setup contacts database + schema
|
||||
pnpm setup:db:calendar # Setup calendar database + schema
|
||||
pnpm setup:db:clock # Setup clock database + schema
|
||||
pnpm setup:db:todo # Setup todo database + schema
|
||||
pnpm setup:db:picture # Setup picture database + schema
|
||||
```
|
||||
|
||||
### Setup All Databases
|
||||
|
||||
```bash
|
||||
pnpm setup:db # Creates ALL databases and pushes ALL schemas
|
||||
```
|
||||
|
||||
This is useful when setting up a fresh environment or after pulling new schema changes.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Docker Init Script
|
||||
|
||||
On first `pnpm docker:up`, the PostgreSQL container runs `docker/init-db/01-create-databases.sql` which creates all databases:
|
||||
|
||||
- manacore, chat, zitare, contacts, calendar, clock, todo, manadeck
|
||||
- storage, mail, moodlit, finance, inventory, techbase, voxel_lava, figgos
|
||||
|
||||
### Setup Script
|
||||
|
||||
The `scripts/setup-databases.sh` script:
|
||||
|
||||
1. **Creates database** if it doesn't exist (using `psql`)
|
||||
2. **Pushes schema** using `drizzle-kit push --force`
|
||||
|
||||
The `--force` flag auto-approves schema changes without interactive prompts.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Database doesn't exist
|
||||
|
||||
If you see `database "xxx" does not exist`:
|
||||
|
||||
```bash
|
||||
# Option 1: Run the setup script
|
||||
pnpm setup:db:chat # or whichever service
|
||||
|
||||
# Option 2: Create manually
|
||||
PGPASSWORD=devpassword psql -h localhost -U manacore -d postgres -c "CREATE DATABASE chat;"
|
||||
```
|
||||
|
||||
### Schema out of date
|
||||
|
||||
If you see errors about missing tables/columns:
|
||||
|
||||
```bash
|
||||
# Push the latest schema
|
||||
pnpm --filter @chat/backend db:push --force
|
||||
```
|
||||
|
||||
### Port already in use
|
||||
|
||||
If auth (port 3001) is already running:
|
||||
|
||||
```bash
|
||||
# Check what's using the port
|
||||
lsof -i :3001
|
||||
|
||||
# Kill the process if needed
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
### Fresh Start (Nuclear Option)
|
||||
|
||||
To completely reset all databases:
|
||||
|
||||
```bash
|
||||
# Stop and remove all containers + volumes
|
||||
pnpm docker:clean
|
||||
|
||||
# Start fresh
|
||||
pnpm docker:up
|
||||
|
||||
# Setup all databases
|
||||
pnpm setup:db
|
||||
```
|
||||
|
||||
## Apps Without Full Commands
|
||||
|
||||
Some apps don't have backends or don't use Drizzle:
|
||||
|
||||
| App | Reason |
|
||||
|-----|--------|
|
||||
| manacore | No backend (uses other services) |
|
||||
| manadeck | Backend exists but no db:push |
|
||||
| bauntown, context, maerchenzauber, memoro, news, nutriphi, presi, quote, reader, storage, wisekeep | No backends |
|
||||
|
||||
For these, use the regular dev commands:
|
||||
|
||||
```bash
|
||||
pnpm dev:manacore:web
|
||||
pnpm dev:manadeck:app
|
||||
```
|
||||
|
||||
## Adding a New Application
|
||||
|
||||
### Step 1: Create Project Structure
|
||||
|
||||
Create the standard project structure under `apps/`:
|
||||
|
||||
```
|
||||
apps/newproject/
|
||||
├── apps/
|
||||
│ ├── backend/ # NestJS API (if needed)
|
||||
│ ├── mobile/ # Expo React Native app
|
||||
│ ├── web/ # SvelteKit web app
|
||||
│ └── landing/ # Astro marketing page
|
||||
├── packages/ # Project-specific shared code
|
||||
├── package.json # Workspace root
|
||||
├── pnpm-workspace.yaml
|
||||
└── CLAUDE.md # Project documentation
|
||||
```
|
||||
|
||||
### Step 2: Configure Backend Database (if applicable)
|
||||
|
||||
If your backend uses Drizzle ORM:
|
||||
|
||||
1. **Add database to Docker init** (`docker/init-db/01-create-databases.sql`):
|
||||
```sql
|
||||
CREATE DATABASE IF NOT EXISTS newproject;
|
||||
GRANT ALL PRIVILEGES ON DATABASE newproject TO manacore;
|
||||
```
|
||||
|
||||
2. **Add to setup script** (`scripts/setup-databases.sh`):
|
||||
|
||||
In the `setup_service()` function, add a new case:
|
||||
```bash
|
||||
newproject)
|
||||
create_db_if_not_exists "newproject"
|
||||
push_schema "@newproject/backend" "newproject"
|
||||
;;
|
||||
```
|
||||
|
||||
Also add to the `ALL_DATABASES` array:
|
||||
```bash
|
||||
ALL_DATABASES=(
|
||||
...
|
||||
"newproject"
|
||||
)
|
||||
```
|
||||
|
||||
And to the services loop at the bottom:
|
||||
```bash
|
||||
for service in auth chat ... newproject; do
|
||||
```
|
||||
|
||||
3. **Add DATABASE_URL to `.env.development`**:
|
||||
```env
|
||||
NEWPROJECT_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/newproject
|
||||
```
|
||||
|
||||
4. **Update `scripts/generate-env.mjs`** to generate the backend `.env` file.
|
||||
|
||||
### Step 3: Add Package.json Scripts
|
||||
|
||||
Add to root `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
// Project-level dev (all apps)
|
||||
"newproject:dev": "turbo run dev --filter=newproject...",
|
||||
|
||||
// Individual app commands
|
||||
"dev:newproject:web": "pnpm --filter @newproject/web dev",
|
||||
"dev:newproject:mobile": "pnpm --filter @newproject/mobile dev",
|
||||
"dev:newproject:backend": "pnpm --filter @newproject/backend dev",
|
||||
"dev:newproject:landing": "pnpm --filter @newproject/landing dev",
|
||||
"dev:newproject:app": "turbo run dev --filter=@newproject/web --filter=@newproject/backend",
|
||||
|
||||
// Full dev with auto database setup
|
||||
"dev:newproject:full": "./scripts/setup-databases.sh newproject && ./scripts/setup-databases.sh auth && concurrently -n auth,backend,web -c blue,green,cyan \"pnpm dev:auth\" \"pnpm dev:newproject:backend\" \"pnpm dev:newproject:web\"",
|
||||
|
||||
// Database shortcuts
|
||||
"newproject:db:push": "pnpm --filter @newproject/backend db:push",
|
||||
"newproject:db:studio": "pnpm --filter @newproject/backend db:studio",
|
||||
|
||||
// Setup shortcut
|
||||
"setup:db:newproject": "./scripts/setup-databases.sh newproject"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Create Project CLAUDE.md
|
||||
|
||||
Create `apps/newproject/CLAUDE.md` with:
|
||||
- Project overview
|
||||
- Structure diagram
|
||||
- Available commands
|
||||
- API endpoints (if backend)
|
||||
- Environment variables
|
||||
- Tech stack details
|
||||
|
||||
See existing projects like `apps/chat/CLAUDE.md` for reference.
|
||||
|
||||
### Step 5: Test the Setup
|
||||
|
||||
```bash
|
||||
# Create database and push schema
|
||||
pnpm setup:db:newproject
|
||||
|
||||
# Start with full dev command
|
||||
pnpm dev:newproject:full
|
||||
```
|
||||
|
||||
## Checklist for New Projects
|
||||
|
||||
- [ ] Create project structure under `apps/newproject/`
|
||||
- [ ] Add `pnpm-workspace.yaml` in project root
|
||||
- [ ] Add database to `docker/init-db/01-create-databases.sql`
|
||||
- [ ] Add service to `scripts/setup-databases.sh`
|
||||
- [ ] Add DATABASE_URL to `.env.development`
|
||||
- [ ] Update `scripts/generate-env.mjs` for env generation
|
||||
- [ ] Add scripts to root `package.json`
|
||||
- [ ] Create `CLAUDE.md` with project documentation
|
||||
- [ ] Test with `pnpm dev:newproject:full`
|
||||
450
docs/SETUP_TEMPLATES.md
Normal file
450
docs/SETUP_TEMPLATES.md
Normal file
|
|
@ -0,0 +1,450 @@
|
|||
# Setup Templates & Checklists
|
||||
|
||||
Quick-reference templates for recurring setup tasks. Copy and customize for new projects.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [New SvelteKit Web App](#1-new-sveltekit-web-app)
|
||||
2. [New NestJS Backend](#2-new-nestjs-backend)
|
||||
3. [Deploying New Service to Staging](#3-deploying-new-service-to-staging)
|
||||
4. [Adding Backend to ManaCore Dashboard](#4-adding-backend-to-manacore-dashboard)
|
||||
5. [Quick Reference Port Assignments](#5-quick-reference-port-assignments)
|
||||
|
||||
---
|
||||
|
||||
## 1. New SvelteKit Web App
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Create `src/hooks.server.ts` with runtime env injection
|
||||
- [ ] Update auth store to use `getAuthUrl()` pattern
|
||||
- [ ] Update user-settings store to use `getAuthUrl()` pattern
|
||||
- [ ] Update any API services to use lazy client initialization
|
||||
- [ ] Add Dockerfile with pnpm symlink preservation
|
||||
- [ ] Add to `docker-compose.staging.yml` with both internal and client URLs
|
||||
- [ ] Test locally with `pnpm dev`
|
||||
- [ ] Deploy and verify `window.__PUBLIC_*__` variables in browser console
|
||||
|
||||
### Template: hooks.server.ts
|
||||
|
||||
```typescript
|
||||
// src/hooks.server.ts
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
// Runtime environment variables for client-side injection
|
||||
const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
|
||||
process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || '';
|
||||
const PUBLIC_BACKEND_URL_CLIENT =
|
||||
process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || '';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) => {
|
||||
const envScript = `<script>
|
||||
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${PUBLIC_MANA_CORE_AUTH_URL_CLIENT}";
|
||||
window.__PUBLIC_BACKEND_URL__ = "${PUBLIC_BACKEND_URL_CLIENT}";
|
||||
</script>`;
|
||||
return html.replace('<head>', `<head>${envScript}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### Template: getAuthUrl() Pattern
|
||||
|
||||
```typescript
|
||||
// src/lib/stores/auth.svelte.ts
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Usage
|
||||
const auth = initializeWebAuth({ baseUrl: getAuthUrl() });
|
||||
```
|
||||
|
||||
### Template: Lazy API Client Initialization
|
||||
|
||||
```typescript
|
||||
// src/lib/api/services/myservice.ts
|
||||
import { browser } from '$app/environment';
|
||||
import { createApiClient } from '../base-client';
|
||||
|
||||
function getApiUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string })
|
||||
.__PUBLIC_BACKEND_URL__;
|
||||
if (injectedUrl) {
|
||||
return `${injectedUrl}/api/v1`;
|
||||
}
|
||||
}
|
||||
return 'http://localhost:3000/api/v1';
|
||||
}
|
||||
|
||||
// IMPORTANT: Lazy initialization - don't create client at module level!
|
||||
let _client: ReturnType<typeof createApiClient> | null = null;
|
||||
|
||||
function getClient() {
|
||||
if (!_client) {
|
||||
_client = createApiClient(getApiUrl());
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
|
||||
export async function getData() {
|
||||
return getClient().get('/data');
|
||||
}
|
||||
```
|
||||
|
||||
### Template: Dockerfile (SvelteKit + pnpm)
|
||||
|
||||
```dockerfile
|
||||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
RUN npm install -g pnpm@9.15.0
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy workspace files
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||
COPY apps/MYPROJECT/apps/web/package.json apps/MYPROJECT/apps/web/
|
||||
COPY packages/ packages/
|
||||
|
||||
# Install all dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy source and build
|
||||
COPY apps/MYPROJECT/apps/web apps/MYPROJECT/apps/web
|
||||
RUN pnpm --filter @myproject/web build
|
||||
|
||||
# Production stage - PRESERVE PNPM SYMLINK STRUCTURE
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Keep same directory structure as builder
|
||||
WORKDIR /app/apps/MYPROJECT/apps/web
|
||||
|
||||
# Copy pnpm store (target of symlinks)
|
||||
COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm
|
||||
|
||||
# Copy app's node_modules (contains symlinks)
|
||||
COPY --from=builder /app/apps/MYPROJECT/apps/web/node_modules ./node_modules
|
||||
|
||||
# Copy built app
|
||||
COPY --from=builder /app/apps/MYPROJECT/apps/web/build ./build
|
||||
COPY --from=builder /app/apps/MYPROJECT/apps/web/package.json ./
|
||||
|
||||
EXPOSE 5173
|
||||
CMD ["node", "build"]
|
||||
```
|
||||
|
||||
### Template: docker-compose.staging.yml Entry
|
||||
|
||||
```yaml
|
||||
myproject-web:
|
||||
image: ghcr.io/memo-2023/myproject-web:${MYPROJECT_WEB_VERSION:-latest}
|
||||
container_name: myproject-web-staging
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '51XX:5173'
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
# Server-side URLs (Docker internal network)
|
||||
PUBLIC_BACKEND_URL: http://myproject-backend:30XX
|
||||
PUBLIC_MANA_CORE_AUTH_URL: http://mana-core-auth:3001
|
||||
# Client-side URLs (browser access via public IP)
|
||||
PUBLIC_BACKEND_URL_CLIENT: http://46.224.108.214:30XX
|
||||
PUBLIC_MANA_CORE_AUTH_URL_CLIENT: http://46.224.108.214:3001
|
||||
depends_on:
|
||||
myproject-backend:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:5173/health']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- manacore-network
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. New NestJS Backend
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Use `text` type for all `user_id` columns (NOT `uuid`)
|
||||
- [ ] Add health check endpoint at `/api/v1/health`
|
||||
- [ ] Configure CORS to include manacore-web origin (port 5173)
|
||||
- [ ] Add database to `docker/init-db/01-create-databases.sql`
|
||||
- [ ] Add to `scripts/setup-databases.sh`
|
||||
- [ ] Add `dev:myproject:full` command to root `package.json`
|
||||
- [ ] Add Dockerfile with correct health check
|
||||
- [ ] Add to `docker-compose.staging.yml` with proper CORS config
|
||||
|
||||
### Template: Drizzle Schema (user_id as text)
|
||||
|
||||
```typescript
|
||||
// src/db/schema/main.schema.ts
|
||||
import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const items = pgTable('items', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
userId: text('user_id').notNull(), // ALWAYS text, not uuid!
|
||||
title: text('title').notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
```
|
||||
|
||||
### Template: Health Controller
|
||||
|
||||
```typescript
|
||||
// src/health/health.controller.ts
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
@Controller('api/v1/health')
|
||||
export class HealthController {
|
||||
@Get()
|
||||
check() {
|
||||
return { status: 'ok', timestamp: new Date().toISOString() };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Template: CORS Configuration
|
||||
|
||||
```typescript
|
||||
// src/main.ts
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
const corsOrigins = process.env.CORS_ORIGINS?.split(',') || [
|
||||
'http://localhost:5173', // Local dev
|
||||
'http://localhost:51XX', // App's own web
|
||||
];
|
||||
|
||||
app.enableCors({
|
||||
origin: corsOrigins,
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
await app.listen(process.env.PORT || 30XX);
|
||||
}
|
||||
```
|
||||
|
||||
### Template: docker-compose.staging.yml Entry
|
||||
|
||||
```yaml
|
||||
myproject-backend:
|
||||
image: ghcr.io/memo-2023/myproject-backend:${MYPROJECT_BACKEND_VERSION:-latest}
|
||||
container_name: myproject-backend-staging
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '30XX:30XX'
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 30XX
|
||||
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/myproject
|
||||
MANA_CORE_AUTH_URL: http://mana-core-auth:3001
|
||||
# CORS - Include app's web AND manacore-web dashboard
|
||||
CORS_ORIGINS: http://46.224.108.214:51XX,http://46.224.108.214:5173,http://localhost:51XX,http://localhost:5173
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
mana-core-auth:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:30XX/api/v1/health']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- manacore-network
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Deploying New Service to Staging
|
||||
|
||||
### Pre-Deployment Checklist
|
||||
|
||||
- [ ] Database exists on staging PostgreSQL
|
||||
- [ ] Dockerfile has correct health check path (`/api/v1/health` for backends)
|
||||
- [ ] `docker-compose.staging.yml` has service definition
|
||||
- [ ] CORS_ORIGINS includes all required origins
|
||||
- [ ] Environment variables set correctly
|
||||
- [ ] Tag format matches project name exactly
|
||||
|
||||
### Create Database on Staging
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/hetzner_deploy_key deploy@46.224.108.214
|
||||
|
||||
# Create database
|
||||
docker exec manacore-postgres-staging psql -U postgres -c 'CREATE DATABASE myproject;'
|
||||
|
||||
# Verify
|
||||
docker exec manacore-postgres-staging psql -U postgres -c '\l' | grep myproject
|
||||
```
|
||||
|
||||
### Deployment Tag Formats
|
||||
|
||||
| Project | Correct Tag Format | Wrong Format |
|
||||
|---------|-------------------|--------------|
|
||||
| mana-core-auth | `mana-core-auth-staging-v1.0.X` | `auth-staging-v1.0.X` |
|
||||
| chat | `chat-staging-v1.0.X` or `chat-all-staging-v1.0.X` | - |
|
||||
| todo | `todo-staging-v1.0.X` or `todo-all-staging-v1.0.X` | - |
|
||||
| calendar | `calendar-staging-v1.0.X` | - |
|
||||
| clock | `clock-staging-v1.0.X` | - |
|
||||
| myproject | `myproject-staging-v1.0.X` | - |
|
||||
|
||||
### Post-Deployment Verification
|
||||
|
||||
```bash
|
||||
# Check container is running correct version
|
||||
docker ps --format '{{.Names}}: {{.Image}}' | grep myproject
|
||||
|
||||
# Check health endpoint
|
||||
curl http://46.224.108.214:30XX/api/v1/health
|
||||
|
||||
# Check logs for errors
|
||||
docker logs myproject-backend-staging --tail 50
|
||||
|
||||
# Test CORS (from manacore-web origin)
|
||||
curl -I -X OPTIONS http://46.224.108.214:30XX/api/v1/endpoint \
|
||||
-H "Origin: http://46.224.108.214:5173" \
|
||||
-H "Access-Control-Request-Method: GET"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Adding Backend to ManaCore Dashboard
|
||||
|
||||
When adding a new backend service that manacore-web dashboard should call:
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Add CORS origin for manacore-web (port 5173) to backend
|
||||
- [ ] Create API service file in `manacore/apps/web/src/lib/api/services/`
|
||||
- [ ] Add runtime URL injection in `manacore/apps/web/src/hooks.server.ts`
|
||||
- [ ] Add environment variables to `docker-compose.staging.yml` for manacore-web
|
||||
- [ ] Deploy both manacore-web and the backend with new config
|
||||
|
||||
### Template: API Service File
|
||||
|
||||
```typescript
|
||||
// apps/manacore/apps/web/src/lib/api/services/myservice.ts
|
||||
import { browser } from '$app/environment';
|
||||
import { createApiClient, type ApiResult } from '../base-client';
|
||||
|
||||
function getMyServiceApiUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MYSERVICE_API_URL__?: string })
|
||||
.__PUBLIC_MYSERVICE_API_URL__;
|
||||
if (injectedUrl) {
|
||||
return `${injectedUrl}/api/v1`;
|
||||
}
|
||||
}
|
||||
return 'http://localhost:30XX/api/v1';
|
||||
}
|
||||
|
||||
let _client: ReturnType<typeof createApiClient> | null = null;
|
||||
|
||||
function getClient() {
|
||||
if (!_client) {
|
||||
_client = createApiClient(getMyServiceApiUrl());
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
|
||||
// Export API functions
|
||||
export async function getItems(): Promise<ApiResult<Item[]>> {
|
||||
return getClient().get('/items');
|
||||
}
|
||||
|
||||
export async function createItem(data: CreateItemDto): Promise<ApiResult<Item>> {
|
||||
return getClient().post('/items', data);
|
||||
}
|
||||
```
|
||||
|
||||
### Template: hooks.server.ts Addition
|
||||
|
||||
```typescript
|
||||
// Add to existing hooks.server.ts
|
||||
const PUBLIC_MYSERVICE_API_URL_CLIENT =
|
||||
process.env.PUBLIC_MYSERVICE_API_URL_CLIENT || process.env.PUBLIC_MYSERVICE_API_URL || '';
|
||||
|
||||
// In transformPageChunk, add:
|
||||
window.__PUBLIC_MYSERVICE_API_URL__ = "${PUBLIC_MYSERVICE_API_URL_CLIENT}";
|
||||
```
|
||||
|
||||
### Template: docker-compose.staging.yml Addition
|
||||
|
||||
```yaml
|
||||
manacore-web:
|
||||
environment:
|
||||
# ... existing env vars ...
|
||||
# Add new backend URL
|
||||
PUBLIC_MYSERVICE_API_URL: http://myservice-backend:30XX
|
||||
PUBLIC_MYSERVICE_API_URL_CLIENT: http://46.224.108.214:30XX
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Quick Reference Port Assignments
|
||||
|
||||
### Backend Ports (3000-3099)
|
||||
|
||||
| Port | Service |
|
||||
|------|---------|
|
||||
| 3000 | chat-web (legacy) |
|
||||
| 3001 | mana-core-auth |
|
||||
| 3002 | chat-backend |
|
||||
| 3006 | picture-backend |
|
||||
| 3007 | zitare-backend |
|
||||
| 3009 | manadeck-backend |
|
||||
| 3015 | contacts-backend |
|
||||
| 3016 | calendar-backend |
|
||||
| 3017 | clock-backend |
|
||||
| 3018 | todo-backend |
|
||||
|
||||
### Web App Ports (5100-5199)
|
||||
|
||||
| Port | Service |
|
||||
|------|---------|
|
||||
| 5173 | manacore-web |
|
||||
| 5175 | picture-web |
|
||||
| 5177 | zitare-web |
|
||||
| 5179 | calendar-web |
|
||||
| 5184 | contacts-web |
|
||||
| 5186 | calendar-web (staging) |
|
||||
| 5187 | clock-web |
|
||||
| 5188 | todo-web |
|
||||
|
||||
### Next Available Ports
|
||||
|
||||
- **Backend**: 3019, 3020, 3021...
|
||||
- **Web**: 5189, 5190, 5191...
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes Quick Reference
|
||||
|
||||
| Mistake | Fix |
|
||||
|---------|-----|
|
||||
| `import.meta.env` in Docker | Use `window.__PUBLIC_*__` injection |
|
||||
| API client at module level | Use lazy `getClient()` pattern |
|
||||
| `uuid` type for user_id | Use `text` type |
|
||||
| Missing CORS for 5173 | Add manacore-web to CORS_ORIGINS |
|
||||
| `auth-staging-v*` tag | Use `mana-core-auth-staging-v*` |
|
||||
| ALTER TABLE without USING | Use `USING column::text` |
|
||||
| `/api/health` endpoint | Use `/api/v1/health` |
|
||||
408
docs/STAGING_DEPLOYMENT_ISSUES.md
Normal file
408
docs/STAGING_DEPLOYMENT_ISSUES.md
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
# Staging Deployment Issues & Solutions
|
||||
|
||||
This document captures common issues encountered during staging deployments and their solutions. Reference this when debugging deployment problems.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Runtime Environment Variables (SvelteKit)](#1-runtime-environment-variables-sveltekit)
|
||||
2. [CORS Configuration](#2-cors-configuration)
|
||||
3. [CD Workflow Version Tags](#3-cd-workflow-version-tags)
|
||||
4. [Database Setup](#4-database-setup)
|
||||
5. [User ID Format (Better Auth)](#5-user-id-format-better-auth)
|
||||
6. [Debugging Checklist](#6-debugging-checklist)
|
||||
7. [Summary: Common Mistakes to Avoid](#summary-common-mistakes-to-avoid)
|
||||
|
||||
---
|
||||
|
||||
## 1. Runtime Environment Variables (SvelteKit)
|
||||
|
||||
### Problem
|
||||
|
||||
SvelteKit apps use `import.meta.env.PUBLIC_*` which gets **baked in at build time**. When running in Docker, the container uses whatever values were present during the GitHub Actions build, not the runtime environment variables.
|
||||
|
||||
**Symptoms:**
|
||||
- Web apps calling `localhost:3001` instead of staging server IP
|
||||
- API calls going to wrong URLs despite correct Docker env vars
|
||||
|
||||
### Solution
|
||||
|
||||
Use **runtime env injection** via `hooks.server.ts`:
|
||||
|
||||
```typescript
|
||||
// src/hooks.server.ts
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
|
||||
process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || '';
|
||||
const PUBLIC_BACKEND_URL_CLIENT =
|
||||
process.env.PUBLIC_BACKEND_URL_CLIENT || '';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) => {
|
||||
const envScript = `<script>
|
||||
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${PUBLIC_MANA_CORE_AUTH_URL_CLIENT}";
|
||||
window.__PUBLIC_BACKEND_URL__ = "${PUBLIC_BACKEND_URL_CLIENT}";
|
||||
</script>`;
|
||||
return html.replace('<head>', `<head>${envScript}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Then in client code, read from `window` instead of `import.meta.env`:
|
||||
|
||||
```typescript
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
function getApiUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as any).__PUBLIC_BACKEND_URL__;
|
||||
if (injectedUrl) return injectedUrl;
|
||||
}
|
||||
return 'http://localhost:3000'; // fallback for local dev
|
||||
}
|
||||
```
|
||||
|
||||
### Lazy Client Initialization Pattern
|
||||
|
||||
**Important**: API clients must be lazily initialized to read the URL at request time, not at module load time:
|
||||
|
||||
```typescript
|
||||
// CORRECT - Lazy initialization
|
||||
let _client: ReturnType<typeof createApiClient> | null = null;
|
||||
|
||||
function getClient() {
|
||||
if (!_client) {
|
||||
_client = createApiClient(getApiUrl()); // URL evaluated when called
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
|
||||
export async function getTasks() {
|
||||
return getClient().get('/tasks'); // Client created on first use
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// WRONG - Module-level initialization
|
||||
const client = createApiClient(getApiUrl()); // URL evaluated at import time!
|
||||
|
||||
export async function getTasks() {
|
||||
return client.get('/tasks'); // Will use stale URL
|
||||
}
|
||||
```
|
||||
|
||||
**Why this matters**: When the module is imported, the `window` object may not have the injected environment variables yet. The lazy pattern ensures the URL is read only when the client is actually needed.
|
||||
|
||||
### Docker Compose Pattern
|
||||
|
||||
Use two environment variables:
|
||||
- `PUBLIC_*_URL` - Internal Docker network URL (container-to-container)
|
||||
- `PUBLIC_*_URL_CLIENT` - External URL for browser access
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
PUBLIC_BACKEND_URL: http://backend-container:3000 # Server-side
|
||||
PUBLIC_BACKEND_URL_CLIENT: http://46.224.108.214:3000 # Browser-side
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. CORS Configuration
|
||||
|
||||
### Problem
|
||||
|
||||
Backends only allow CORS from their own web apps, blocking requests from other origins like manacore-web dashboard.
|
||||
|
||||
**Symptoms:**
|
||||
- `Access to fetch blocked by CORS policy`
|
||||
- `No 'Access-Control-Allow-Origin' header`
|
||||
|
||||
### Solution
|
||||
|
||||
Add all necessary origins to `CORS_ORIGINS` in docker-compose.staging.yml:
|
||||
|
||||
```yaml
|
||||
todo-backend:
|
||||
environment:
|
||||
# Include both the app's own web AND manacore-web dashboard
|
||||
CORS_ORIGINS: http://46.224.108.214:5188,http://46.224.108.214:5173,http://localhost:5188,http://localhost:5173
|
||||
```
|
||||
|
||||
### Checklist for New Backends
|
||||
|
||||
When deploying a new backend that will be called from manacore-web dashboard:
|
||||
1. Add `http://46.224.108.214:5173` to CORS_ORIGINS
|
||||
2. Add `http://localhost:5173` for local development
|
||||
3. Restart the container after config changes
|
||||
|
||||
### Testing CORS
|
||||
|
||||
```bash
|
||||
curl -I -X OPTIONS http://46.224.108.214:3018/api/v1/endpoint \
|
||||
-H "Origin: http://46.224.108.214:5173" \
|
||||
-H "Access-Control-Request-Method: GET"
|
||||
|
||||
# Should see:
|
||||
# Access-Control-Allow-Origin: http://46.224.108.214:5173
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. CD Workflow Version Tags
|
||||
|
||||
### Problem
|
||||
|
||||
docker-compose uses variables like `${TODO_WEB_VERSION:-latest}`, but the CD workflow wasn't updating the `.env` file on the staging server, causing containers to always use `latest` instead of the tagged version.
|
||||
|
||||
**Symptoms:**
|
||||
- Deployed new version but container still running old code
|
||||
- `docker ps` shows wrong image tag
|
||||
|
||||
### Solution
|
||||
|
||||
The CD workflow (`.github/workflows/cd-staging-tagged.yml`) now:
|
||||
1. Computes the version variable name (e.g., `TODO_WEB_VERSION`)
|
||||
2. Updates the `.env` file on staging server
|
||||
3. docker-compose reads from `.env`
|
||||
|
||||
### Tag Naming Convention
|
||||
|
||||
Tags must follow the exact project name as defined in the CD workflow:
|
||||
|
||||
| Project | Correct Tag Format | Wrong Format |
|
||||
|---------|-------------------|--------------|
|
||||
| mana-core-auth | `mana-core-auth-staging-v1.0.0` | `auth-staging-v1.0.0` |
|
||||
| chat | `chat-staging-v1.0.0` or `chat-all-staging-v1.0.0` | - |
|
||||
| todo | `todo-staging-v1.0.0` or `todo-all-staging-v1.0.0` | - |
|
||||
|
||||
**Note**: Using the wrong tag format (e.g., `auth-staging-*` instead of `mana-core-auth-staging-*`) will cause the workflow to fail because it won't find the correct Dockerfile path.
|
||||
|
||||
### Verifying Deployment
|
||||
|
||||
```bash
|
||||
# Check running container version
|
||||
docker ps --format '{{.Names}}: {{.Image}}' | grep todo
|
||||
|
||||
# Check .env file
|
||||
cat ~/manacore-staging/.env | grep VERSION
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Database Setup
|
||||
|
||||
### Problem
|
||||
|
||||
New backends fail with `database "X" does not exist` because the PostgreSQL databases weren't created.
|
||||
|
||||
**Symptoms:**
|
||||
- 500 Internal Server Error
|
||||
- Logs show: `PostgresError: database "todo" does not exist`
|
||||
|
||||
### Solution
|
||||
|
||||
Create databases manually on first deployment:
|
||||
|
||||
```bash
|
||||
# SSH to staging
|
||||
ssh deploy@46.224.108.214
|
||||
|
||||
# Create databases
|
||||
docker exec manacore-postgres-staging psql -U postgres -c 'CREATE DATABASE todo;'
|
||||
docker exec manacore-postgres-staging psql -U postgres -c 'CREATE DATABASE calendar;'
|
||||
docker exec manacore-postgres-staging psql -U postgres -c 'CREATE DATABASE clock;'
|
||||
|
||||
# Restart backends (they auto-migrate schemas on startup)
|
||||
cd ~/manacore-staging
|
||||
docker compose restart todo-backend calendar-backend clock-backend
|
||||
```
|
||||
|
||||
### Checklist for New Apps
|
||||
|
||||
When deploying a new app with a database:
|
||||
1. Create the database: `CREATE DATABASE appname;`
|
||||
2. The backend will auto-migrate the schema on startup
|
||||
3. Verify tables exist: `\dt` in psql
|
||||
|
||||
---
|
||||
|
||||
## 5. User ID Format (Better Auth)
|
||||
|
||||
### Problem
|
||||
|
||||
Backend database schemas use `uuid` type for `user_id`, but Better Auth generates non-UUID user IDs like `otUe1YrfENPdHnrF3g1vSBfpkQfambCZ`.
|
||||
|
||||
**Symptoms:**
|
||||
- 500 Internal Server Error on authenticated requests
|
||||
- Logs show: `invalid input syntax for type uuid: "otUe1YrfENPdHnrF3g1vSBfpkQfambCZ"`
|
||||
|
||||
### Solution
|
||||
|
||||
Change `user_id` columns from `uuid` to `text`:
|
||||
|
||||
```sql
|
||||
-- For each table with user_id (use USING clause for explicit conversion)
|
||||
ALTER TABLE tasks ALTER COLUMN user_id TYPE text USING user_id::text;
|
||||
ALTER TABLE projects ALTER COLUMN user_id TYPE text USING user_id::text;
|
||||
-- etc.
|
||||
```
|
||||
|
||||
**Important**: Always use the `USING` clause when converting column types. Without it, PostgreSQL may silently fail or produce unexpected results:
|
||||
|
||||
```sql
|
||||
-- CORRECT - Explicit conversion
|
||||
ALTER TABLE events ALTER COLUMN user_id TYPE text USING user_id::text;
|
||||
|
||||
-- RISKY - May fail silently on some data types
|
||||
ALTER TABLE events ALTER COLUMN user_id TYPE text;
|
||||
```
|
||||
|
||||
### Prevention
|
||||
|
||||
When creating new backend schemas, **always use `text` type for user_id**:
|
||||
|
||||
```typescript
|
||||
// Drizzle schema - CORRECT
|
||||
export const tasks = pgTable('tasks', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
userId: text('user_id').notNull(), // Use text, not uuid
|
||||
// ...
|
||||
});
|
||||
|
||||
// WRONG - Don't do this
|
||||
export const tasks = pgTable('tasks', {
|
||||
userId: uuid('user_id').notNull(), // Will fail with Better Auth
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Debugging Commands
|
||||
|
||||
```bash
|
||||
# Check container logs
|
||||
docker logs <container-name> --tail 50
|
||||
|
||||
# Check container is running correct version
|
||||
docker ps --format '{{.Names}}: {{.Image}}'
|
||||
|
||||
# Test CORS
|
||||
curl -I -X OPTIONS <url> -H "Origin: <origin>"
|
||||
|
||||
# Check database exists
|
||||
docker exec manacore-postgres-staging psql -U postgres -c '\l'
|
||||
|
||||
# Check tables in database
|
||||
docker exec manacore-postgres-staging psql -U postgres -d <dbname> -c '\dt'
|
||||
|
||||
# Restart a service
|
||||
cd ~/manacore-staging && docker compose restart <service-name>
|
||||
|
||||
# Force recreate with new config
|
||||
cd ~/manacore-staging && docker compose up -d --no-deps --force-recreate <service-name>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Port Reference
|
||||
|
||||
| Service | Port |
|
||||
|---------|------|
|
||||
| mana-core-auth | 3001 |
|
||||
| chat-backend | 3002 |
|
||||
| calendar-backend | 3016 |
|
||||
| clock-backend | 3017 |
|
||||
| todo-backend | 3018 |
|
||||
| chat-web | 3000 |
|
||||
| manacore-web | 5173 |
|
||||
| calendar-web | 5186 |
|
||||
| clock-web | 5187 |
|
||||
| todo-web | 5188 |
|
||||
|
||||
---
|
||||
|
||||
## 6. Debugging Checklist
|
||||
|
||||
When something doesn't work on staging, follow this checklist:
|
||||
|
||||
### API Returns Wrong Data or Fails
|
||||
|
||||
1. **Check if calling correct URL**
|
||||
```bash
|
||||
# In browser console
|
||||
console.log(window.__PUBLIC_BACKEND_URL__)
|
||||
```
|
||||
If undefined or localhost, the runtime env injection isn't working.
|
||||
|
||||
2. **Check CORS**
|
||||
```bash
|
||||
curl -I -X OPTIONS http://46.224.108.214:<port>/api/v1/endpoint \
|
||||
-H "Origin: http://46.224.108.214:5173"
|
||||
```
|
||||
Should return `Access-Control-Allow-Origin` header.
|
||||
|
||||
3. **Check container logs**
|
||||
```bash
|
||||
ssh deploy@46.224.108.214 "docker logs <container-name> --tail 100"
|
||||
```
|
||||
|
||||
### 500 Internal Server Error
|
||||
|
||||
1. **Check database exists**
|
||||
```bash
|
||||
docker exec manacore-postgres-staging psql -U postgres -c '\l'
|
||||
```
|
||||
|
||||
2. **Check tables exist**
|
||||
```bash
|
||||
docker exec manacore-postgres-staging psql -U postgres -d <dbname> -c '\dt'
|
||||
```
|
||||
|
||||
3. **Check for type mismatches** (especially user_id uuid vs text)
|
||||
|
||||
### 401 Unauthorized
|
||||
|
||||
1. **Check token is being sent**
|
||||
```bash
|
||||
# In browser Network tab, check Authorization header
|
||||
```
|
||||
|
||||
2. **Check JWKS endpoint**
|
||||
```bash
|
||||
curl http://46.224.108.214:3001/api/v1/auth/jwks
|
||||
```
|
||||
|
||||
3. **Check issuer/audience match** - Token must have `iss: manacore` and `aud: manacore`
|
||||
|
||||
### Container Not Updated
|
||||
|
||||
1. **Check image version**
|
||||
```bash
|
||||
docker ps --format '{{.Names}}: {{.Image}}'
|
||||
```
|
||||
|
||||
2. **Check .env file**
|
||||
```bash
|
||||
cat ~/manacore-staging/.env | grep VERSION
|
||||
```
|
||||
|
||||
3. **Force recreate**
|
||||
```bash
|
||||
docker compose up -d --no-deps --force-recreate <service-name>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary: Common Mistakes to Avoid
|
||||
|
||||
| Mistake | Consequence | Prevention |
|
||||
|---------|-------------|------------|
|
||||
| Using `import.meta.env` for Docker runtime | URLs baked at build time | Use `window.__PUBLIC_*__` with runtime injection |
|
||||
| Initializing API clients at module level | Client uses stale URLs | Use lazy initialization pattern |
|
||||
| Using `uuid` type for user_id | Better Auth IDs fail validation | Always use `text` type for user_id |
|
||||
| Missing CORS origin for manacore-web | Dashboard can't call backends | Add port 5173 to all backend CORS configs |
|
||||
| Wrong tag format for mana-core-auth | Deployment fails, can't find Dockerfile | Use `mana-core-auth-staging-v*` not `auth-staging-v*` |
|
||||
| Forgetting to create database | Backend crashes on startup | Create database before first deployment |
|
||||
| ALTER TABLE without USING clause | Silent failures on type conversion | Always use `USING column::new_type` |
|
||||
11
package.json
11
package.json
|
|
@ -17,6 +17,9 @@
|
|||
"format": "prettier --config .prettierrc.json --write \"**/*.{ts,tsx,js,jsx,json,md,svelte,astro}\"",
|
||||
"format:check": "prettier --config .prettierrc.json --check \"**/*.{ts,tsx,js,jsx,json,md,svelte,astro}\"",
|
||||
"setup:env": "node scripts/generate-env.mjs",
|
||||
"setup:db": "./scripts/setup-databases.sh",
|
||||
"setup:db:chat": "./scripts/setup-databases.sh chat",
|
||||
"setup:db:auth": "./scripts/setup-databases.sh auth",
|
||||
"build:packages": "pnpm --filter '@manacore/*' build",
|
||||
"postinstall": "node scripts/generate-env.mjs || true && pnpm run build:packages || true",
|
||||
"manacore:dev": "turbo run dev --filter=manacore...",
|
||||
|
|
@ -39,25 +42,28 @@
|
|||
"dev:picture:mobile": "pnpm --filter @picture/mobile dev",
|
||||
"dev:picture:backend": "pnpm --filter @picture/backend dev",
|
||||
"dev:picture:app": "turbo run dev --filter=@picture/web --filter=@picture/backend",
|
||||
"dev:picture:full": "./scripts/setup-databases.sh picture && ./scripts/setup-databases.sh auth && concurrently -n auth,backend,web -c blue,green,cyan \"pnpm dev:auth\" \"pnpm dev:picture:backend\" \"pnpm dev:picture:web\"",
|
||||
"dev:chat:mobile": "pnpm --filter @chat/mobile dev",
|
||||
"dev:chat:web": "pnpm --filter @chat/web dev",
|
||||
"dev:chat:landing": "pnpm --filter @chat/landing dev",
|
||||
"dev:chat:backend": "pnpm --filter @chat/backend start:dev",
|
||||
"dev:chat:app": "turbo run dev --filter=@chat/web --filter=@chat/backend",
|
||||
"dev:auth": "pnpm --filter mana-core-auth start:dev",
|
||||
"dev:chat:full": "concurrently \"pnpm dev:auth\" \"pnpm dev:chat:backend\"",
|
||||
"dev:chat:full": "./scripts/setup-databases.sh chat && ./scripts/setup-databases.sh auth && concurrently -n auth,backend,web -c blue,green,cyan \"pnpm dev:auth\" \"pnpm dev:chat:backend\" \"pnpm dev:chat:web\"",
|
||||
"zitare:dev": "turbo run dev --filter=zitare...",
|
||||
"dev:zitare:mobile": "pnpm --filter @zitare/mobile dev",
|
||||
"dev:zitare:web": "pnpm --filter @zitare/web dev",
|
||||
"dev:zitare:landing": "pnpm --filter @zitare/landing dev",
|
||||
"dev:zitare:backend": "pnpm --filter @zitare/backend dev",
|
||||
"dev:zitare:app": "turbo run dev --filter=@zitare/web --filter=@zitare/backend",
|
||||
"dev:zitare:full": "./scripts/setup-databases.sh zitare && ./scripts/setup-databases.sh auth && concurrently -n auth,backend,web -c blue,green,cyan \"pnpm dev:auth\" \"pnpm dev:zitare:backend\" \"pnpm dev:zitare:web\"",
|
||||
"contacts:dev": "turbo run dev --filter=contacts...",
|
||||
"dev:contacts:mobile": "pnpm --filter @contacts/mobile dev",
|
||||
"dev:contacts:web": "pnpm --filter @contacts/web dev",
|
||||
"dev:contacts:landing": "pnpm --filter @contacts/landing dev",
|
||||
"dev:contacts:backend": "pnpm --filter @contacts/backend dev",
|
||||
"dev:contacts:app": "turbo run dev --filter=@contacts/web --filter=@contacts/backend",
|
||||
"dev:contacts:full": "./scripts/setup-databases.sh contacts && ./scripts/setup-databases.sh auth && concurrently -n auth,backend,web -c blue,green,cyan \"pnpm dev:auth\" \"pnpm dev:contacts:backend\" \"pnpm dev:contacts:web\"",
|
||||
"contacts:db:push": "pnpm --filter @contacts/backend db:push",
|
||||
"contacts:db:studio": "pnpm --filter @contacts/backend db:studio",
|
||||
"contacts:db:seed": "pnpm --filter @contacts/backend db:seed",
|
||||
|
|
@ -67,6 +73,7 @@
|
|||
"dev:calendar:landing": "pnpm --filter @calendar/landing dev",
|
||||
"dev:calendar:backend": "pnpm --filter @calendar/backend dev",
|
||||
"dev:calendar:app": "turbo run dev --filter=@calendar/web --filter=@calendar/backend",
|
||||
"dev:calendar:full": "./scripts/setup-databases.sh calendar && ./scripts/setup-databases.sh auth && concurrently -n auth,backend,web -c blue,green,cyan \"pnpm dev:auth\" \"pnpm dev:calendar:backend\" \"pnpm dev:calendar:web\"",
|
||||
"calendar:db:push": "pnpm --filter @calendar/backend db:push",
|
||||
"calendar:db:studio": "pnpm --filter @calendar/backend db:studio",
|
||||
"calendar:db:seed": "pnpm --filter @calendar/backend db:seed",
|
||||
|
|
@ -75,6 +82,7 @@
|
|||
"dev:clock:landing": "pnpm --filter @clock/landing dev",
|
||||
"dev:clock:backend": "pnpm --filter @clock/backend dev",
|
||||
"dev:clock:app": "turbo run dev --filter=@clock/web --filter=@clock/backend",
|
||||
"dev:clock:full": "./scripts/setup-databases.sh clock && ./scripts/setup-databases.sh auth && concurrently -n auth,backend,web -c blue,green,cyan \"pnpm dev:auth\" \"pnpm dev:clock:backend\" \"pnpm dev:clock:web\"",
|
||||
"clock:db:push": "pnpm --filter @clock/backend db:push",
|
||||
"clock:db:studio": "pnpm --filter @clock/backend db:studio",
|
||||
"clock:db:seed": "pnpm --filter @clock/backend db:seed",
|
||||
|
|
@ -92,6 +100,7 @@
|
|||
"dev:todo:landing": "pnpm --filter @todo/landing dev",
|
||||
"dev:todo:backend": "pnpm --filter @todo/backend dev",
|
||||
"dev:todo:app": "turbo run dev --filter=@todo/web --filter=@todo/backend",
|
||||
"dev:todo:full": "./scripts/setup-databases.sh todo && ./scripts/setup-databases.sh auth && concurrently -n auth,backend,web -c blue,green,cyan \"pnpm dev:auth\" \"pnpm dev:todo:backend\" \"pnpm dev:todo:web\"",
|
||||
"todo:db:push": "pnpm --filter @todo/backend db:push",
|
||||
"todo:db:studio": "pnpm --filter @todo/backend db:studio",
|
||||
"todo:db:seed": "pnpm --filter @todo/backend db:seed",
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ const DEFAULT_ENDPOINTS: AuthEndpoints = {
|
|||
refresh: '/api/v1/auth/refresh',
|
||||
validate: '/api/v1/auth/validate',
|
||||
forgotPassword: '/api/v1/auth/forgot-password',
|
||||
resetPassword: '/api/v1/auth/reset-password',
|
||||
googleSignIn: '/api/v1/auth/google-signin',
|
||||
appleSignIn: '/api/v1/auth/apple-signin',
|
||||
credits: '/api/v1/credits/balance',
|
||||
|
|
@ -192,6 +193,41 @@ export function createAuthService(config: AuthServiceConfig) {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset password with token
|
||||
*/
|
||||
async resetPassword(token: string, newPassword: string): Promise<AuthResult> {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}${endpoints.resetPassword}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token, newPassword }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
|
||||
if (errorData.message?.includes('expired')) {
|
||||
return { success: false, error: 'Reset link has expired. Please request a new one.' };
|
||||
}
|
||||
|
||||
if (errorData.message?.includes('invalid')) {
|
||||
return { success: false, error: 'Invalid reset link. Please request a new one.' };
|
||||
}
|
||||
|
||||
return { success: false, error: errorData.message || 'Password reset failed' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error resetting password:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error during password reset',
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh the authentication tokens
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ export interface AuthEndpoints {
|
|||
refresh: string;
|
||||
validate: string;
|
||||
forgotPassword: string;
|
||||
resetPassword: string;
|
||||
googleSignIn: string;
|
||||
appleSignIn: string;
|
||||
credits: string;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export {
|
|||
MailLogo,
|
||||
MoodlitLogo,
|
||||
InventoryLogo,
|
||||
ClockLogo,
|
||||
} from './logos';
|
||||
|
||||
// Configuration
|
||||
|
|
|
|||
13
packages/shared-branding/src/logos/ClockLogo.svelte
Normal file
13
packages/shared-branding/src/logos/ClockLogo.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import AppLogo from '../AppLogo.svelte';
|
||||
|
||||
interface Props {
|
||||
size?: number;
|
||||
color?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { size = 55, color, class: className = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<AppLogo app="clock" {size} {color} class={className} />
|
||||
|
|
@ -18,3 +18,4 @@ export { default as TodoLogo } from './TodoLogo.svelte';
|
|||
export { default as MailLogo } from './MailLogo.svelte';
|
||||
export { default as MoodlitLogo } from './MoodlitLogo.svelte';
|
||||
export { default as InventoryLogo } from './InventoryLogo.svelte';
|
||||
export { default as ClockLogo } from './ClockLogo.svelte';
|
||||
|
|
|
|||
942
pnpm-lock.yaml
generated
942
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
169
scripts/setup-databases.sh
Executable file
169
scripts/setup-databases.sh
Executable file
|
|
@ -0,0 +1,169 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Setup script for creating databases and pushing schemas
|
||||
# Usage: ./scripts/setup-databases.sh [service]
|
||||
# Examples:
|
||||
# ./scripts/setup-databases.sh # Setup all
|
||||
# ./scripts/setup-databases.sh chat # Setup only chat
|
||||
# ./scripts/setup-databases.sh auth # Setup only auth
|
||||
|
||||
set -e
|
||||
|
||||
# Database connection details (from .env.development)
|
||||
DB_HOST="${DB_HOST:-localhost}"
|
||||
DB_PORT="${DB_PORT:-5432}"
|
||||
DB_USER="${POSTGRES_USER:-manacore}"
|
||||
DB_PASSWORD="${POSTGRES_PASSWORD:-devpassword}"
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}🗄️ Database Setup Script${NC}"
|
||||
echo "======================================"
|
||||
|
||||
# Function to create database if it doesn't exist
|
||||
create_db_if_not_exists() {
|
||||
local db_name=$1
|
||||
echo -e "${YELLOW}Checking database: ${db_name}${NC}"
|
||||
|
||||
if PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d postgres -tc \
|
||||
"SELECT 1 FROM pg_database WHERE datname = '$db_name'" | grep -q 1; then
|
||||
echo -e " ${GREEN}✓ Exists${NC}"
|
||||
else
|
||||
echo -e " Creating database ${db_name}..."
|
||||
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d postgres -c "CREATE DATABASE $db_name;" > /dev/null
|
||||
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d postgres -c "GRANT ALL PRIVILEGES ON DATABASE $db_name TO $DB_USER;" > /dev/null
|
||||
echo -e " ${GREEN}✓ Created${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to push schema for a service
|
||||
push_schema() {
|
||||
local filter=$1
|
||||
local name=$2
|
||||
echo -e "${YELLOW}Pushing schema for ${name}...${NC}"
|
||||
# Use --force to auto-approve in development (skips interactive prompts)
|
||||
if pnpm --filter "$filter" db:push --force 2>/dev/null; then
|
||||
echo -e " ${GREEN}✓ Schema pushed${NC}"
|
||||
else
|
||||
echo -e " ${RED}✗ Failed (may not have db:push script)${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# All databases that should exist
|
||||
ALL_DATABASES=(
|
||||
"manacore"
|
||||
"chat"
|
||||
"zitare"
|
||||
"contacts"
|
||||
"calendar"
|
||||
"clock"
|
||||
"todo"
|
||||
"manadeck"
|
||||
"storage"
|
||||
"mail"
|
||||
"moodlit"
|
||||
"finance"
|
||||
"inventory"
|
||||
"techbase"
|
||||
"voxel_lava"
|
||||
"figgos"
|
||||
)
|
||||
|
||||
# Check if specific service requested
|
||||
SERVICE_FILTER=${1:-""}
|
||||
|
||||
setup_service() {
|
||||
local service=$1
|
||||
|
||||
case $service in
|
||||
auth|mana-core-auth)
|
||||
create_db_if_not_exists "manacore"
|
||||
push_schema "mana-core-auth" "mana-core-auth"
|
||||
;;
|
||||
chat)
|
||||
create_db_if_not_exists "chat"
|
||||
push_schema "@chat/backend" "chat"
|
||||
;;
|
||||
zitare)
|
||||
create_db_if_not_exists "zitare"
|
||||
push_schema "@zitare/backend" "zitare"
|
||||
;;
|
||||
contacts)
|
||||
create_db_if_not_exists "contacts"
|
||||
push_schema "@contacts/backend" "contacts"
|
||||
;;
|
||||
calendar)
|
||||
create_db_if_not_exists "calendar"
|
||||
push_schema "@calendar/backend" "calendar"
|
||||
;;
|
||||
clock)
|
||||
create_db_if_not_exists "clock"
|
||||
push_schema "@clock/backend" "clock"
|
||||
;;
|
||||
todo)
|
||||
create_db_if_not_exists "todo"
|
||||
push_schema "@todo/backend" "todo"
|
||||
;;
|
||||
manadeck)
|
||||
create_db_if_not_exists "manadeck"
|
||||
push_schema "@manadeck/backend" "manadeck"
|
||||
;;
|
||||
mail)
|
||||
create_db_if_not_exists "mail"
|
||||
push_schema "@mail/backend" "mail"
|
||||
;;
|
||||
moodlit)
|
||||
create_db_if_not_exists "moodlit"
|
||||
push_schema "@moodlit/backend" "moodlit"
|
||||
;;
|
||||
picture)
|
||||
create_db_if_not_exists "picture"
|
||||
push_schema "@picture/backend" "picture"
|
||||
;;
|
||||
finance)
|
||||
create_db_if_not_exists "finance"
|
||||
push_schema "@finance/backend" "finance"
|
||||
;;
|
||||
voxel-lava)
|
||||
create_db_if_not_exists "voxel_lava"
|
||||
push_schema "@voxel-lava/backend" "voxel-lava"
|
||||
;;
|
||||
figgos)
|
||||
create_db_if_not_exists "figgos"
|
||||
push_schema "@figgos/backend" "figgos"
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown service: $service${NC}"
|
||||
echo "Available services: auth, chat, zitare, contacts, calendar, clock, todo, manadeck, mail, moodlit, finance, voxel-lava, figgos"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ -n "$SERVICE_FILTER" ]; then
|
||||
echo -e "Setting up for service: ${SERVICE_FILTER}"
|
||||
setup_service "$SERVICE_FILTER"
|
||||
echo -e "\n${GREEN}✓ Setup complete!${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Setup all databases
|
||||
echo -e "\n${GREEN}Step 1: Creating databases${NC}"
|
||||
echo "--------------------------------------"
|
||||
for db in "${ALL_DATABASES[@]}"; do
|
||||
create_db_if_not_exists "$db"
|
||||
done
|
||||
|
||||
echo -e "\n${GREEN}Step 2: Pushing schemas${NC}"
|
||||
echo "--------------------------------------"
|
||||
|
||||
# Push schemas for all known services
|
||||
for service in auth chat zitare contacts calendar clock todo manadeck picture mail moodlit finance voxel-lava figgos; do
|
||||
setup_service "$service" 2>/dev/null || true
|
||||
done
|
||||
|
||||
echo -e "\n${GREEN}✓ Database setup complete!${NC}"
|
||||
|
|
@ -91,7 +91,9 @@ services/mana-core-auth/
|
|||
│ ├── credits/ # Credit system
|
||||
│ ├── db/
|
||||
│ │ ├── schema/ # Drizzle schemas
|
||||
│ │ └── connection.ts # DB connection
|
||||
│ │ ├── migrations/ # Generated migration files
|
||||
│ │ ├── connection.ts # DB connection
|
||||
│ │ └── migrate.ts # Migration script with advisory locks
|
||||
│ └── config/
|
||||
│ └── configuration.ts # App config
|
||||
├── docs/
|
||||
|
|
@ -99,6 +101,16 @@ services/mana-core-auth/
|
|||
└── test/
|
||||
```
|
||||
|
||||
## Database Migrations
|
||||
|
||||
For comprehensive migration documentation, see **[docs/DATABASE_MIGRATIONS.md](/docs/DATABASE_MIGRATIONS.md)**.
|
||||
|
||||
Key points:
|
||||
- Use `db:push` for development (fast iteration)
|
||||
- Use `db:generate` + `db:migrate` for production (tracked migrations)
|
||||
- Migrations use advisory locks to prevent concurrent execution
|
||||
- CI/CD runs migrations automatically before code deployment
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
"test:cov": "jest --coverage",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "tsx src/db/migrate.ts",
|
||||
"db:studio": "drizzle-kit studio"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import { RegisterB2BDto } from './dto/register-b2b.dto';
|
|||
import { InviteEmployeeDto } from './dto/invite-employee.dto';
|
||||
import { AcceptInvitationDto } from './dto/accept-invitation.dto';
|
||||
import { SetActiveOrganizationDto } from './dto/set-active-organization.dto';
|
||||
import { ForgotPasswordDto } from './dto/forgot-password.dto';
|
||||
import { ResetPasswordDto } from './dto/reset-password.dto';
|
||||
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||
|
||||
/**
|
||||
|
|
@ -137,6 +139,39 @@ export class AuthController {
|
|||
return this.betterAuthService.getJwks();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Password Reset Endpoints
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Request password reset
|
||||
*
|
||||
* Initiates the password reset flow by sending an email with a reset link.
|
||||
* Always returns success to prevent email enumeration attacks.
|
||||
*/
|
||||
@Post('forgot-password')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) {
|
||||
return this.betterAuthService.requestPasswordReset(
|
||||
forgotPasswordDto.email,
|
||||
forgotPasswordDto.redirectTo
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password with token
|
||||
*
|
||||
* Completes the password reset using the token from the email link.
|
||||
*/
|
||||
@Post('reset-password')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async resetPassword(@Body() resetPasswordDto: ResetPasswordDto) {
|
||||
return this.betterAuthService.resetPassword(
|
||||
resetPasswordDto.token,
|
||||
resetPasswordDto.newPassword
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// B2B Registration
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -80,12 +80,36 @@ export function createBetterAuth(databaseUrl: string) {
|
|||
},
|
||||
}),
|
||||
|
||||
// Email/password authentication only
|
||||
// Email/password authentication with password reset
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: false, // Can enable later
|
||||
minPasswordLength: 12,
|
||||
maxPasswordLength: 128,
|
||||
|
||||
/**
|
||||
* Password Reset Configuration
|
||||
*
|
||||
* Better Auth provides password reset via:
|
||||
* - auth.api.forgetPassword({ email }) - Sends reset email
|
||||
* - auth.api.resetPassword({ newPassword, token }) - Resets password
|
||||
*
|
||||
* @see https://www.better-auth.com/docs/authentication/email-password#password-reset
|
||||
*/
|
||||
sendResetPassword: async ({ user, url, token }) => {
|
||||
// TODO: Implement email sending service (e.g., Resend, SendGrid)
|
||||
// For now, log the reset URL for development
|
||||
console.log('[Password Reset] User:', user.email);
|
||||
console.log('[Password Reset] Reset URL:', url);
|
||||
console.log('[Password Reset] Token:', token);
|
||||
|
||||
// In production, send an email like:
|
||||
// await sendEmail({
|
||||
// to: user.email,
|
||||
// subject: 'Reset your password',
|
||||
// html: `<a href="${url}">Reset your password</a>`
|
||||
// });
|
||||
},
|
||||
},
|
||||
|
||||
// Session configuration
|
||||
|
|
|
|||
22
services/mana-core-auth/src/auth/dto/forgot-password.dto.ts
Normal file
22
services/mana-core-auth/src/auth/dto/forgot-password.dto.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { IsEmail, IsOptional, IsString, IsUrl } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Forgot Password DTO
|
||||
*
|
||||
* Request body for initiating password reset.
|
||||
*/
|
||||
export class ForgotPasswordDto {
|
||||
/**
|
||||
* User's email address
|
||||
*/
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
/**
|
||||
* Optional redirect URL after password reset
|
||||
* The reset token will be appended as a query parameter
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
redirectTo?: string;
|
||||
}
|
||||
22
services/mana-core-auth/src/auth/dto/reset-password.dto.ts
Normal file
22
services/mana-core-auth/src/auth/dto/reset-password.dto.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { IsString, MinLength, MaxLength } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Reset Password DTO
|
||||
*
|
||||
* Request body for resetting password with token.
|
||||
*/
|
||||
export class ResetPasswordDto {
|
||||
/**
|
||||
* Reset token from email link
|
||||
*/
|
||||
@IsString()
|
||||
token: string;
|
||||
|
||||
/**
|
||||
* New password (must meet password requirements)
|
||||
*/
|
||||
@IsString()
|
||||
@MinLength(12, { message: 'Password must be at least 12 characters long' })
|
||||
@MaxLength(128, { message: 'Password must be at most 128 characters long' })
|
||||
newPassword: string;
|
||||
}
|
||||
|
|
@ -845,6 +845,92 @@ export class BetterAuthService {
|
|||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Password Reset Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Request password reset
|
||||
*
|
||||
* Sends a password reset email to the user.
|
||||
* Uses Better Auth's forgetPassword API.
|
||||
*
|
||||
* @param email - User's email address
|
||||
* @param redirectTo - Optional URL to redirect after reset (used in email link)
|
||||
* @returns Success status
|
||||
*/
|
||||
async requestPasswordReset(
|
||||
email: string,
|
||||
redirectTo?: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
// Better Auth's forgetPassword method
|
||||
// See: https://www.better-auth.com/docs/authentication/email-password#password-reset
|
||||
await (this.auth.api as any).forgetPassword({
|
||||
body: {
|
||||
email,
|
||||
redirectTo,
|
||||
},
|
||||
});
|
||||
|
||||
// Always return success to prevent email enumeration
|
||||
return {
|
||||
success: true,
|
||||
message: 'If an account with that email exists, a password reset link has been sent',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[requestPasswordReset] Error:', error);
|
||||
// Always return success to prevent email enumeration attacks
|
||||
return {
|
||||
success: true,
|
||||
message: 'If an account with that email exists, a password reset link has been sent',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password with token
|
||||
*
|
||||
* Resets the user's password using the token from the reset email.
|
||||
* Uses Better Auth's resetPassword API.
|
||||
*
|
||||
* @param token - Reset token from email link
|
||||
* @param newPassword - New password to set
|
||||
* @returns Success status
|
||||
* @throws UnauthorizedException if token is invalid or expired
|
||||
*/
|
||||
async resetPassword(
|
||||
token: string,
|
||||
newPassword: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
// Better Auth's resetPassword method
|
||||
// See: https://www.better-auth.com/docs/authentication/email-password#password-reset
|
||||
await (this.auth.api as any).resetPassword({
|
||||
body: {
|
||||
token,
|
||||
newPassword,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Password has been reset successfully',
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
if (
|
||||
error.message?.includes('invalid') ||
|
||||
error.message?.includes('expired') ||
|
||||
error.message?.includes('not found')
|
||||
) {
|
||||
throw new UnauthorizedException('Invalid or expired reset token');
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JWKS (JSON Web Key Set)
|
||||
*
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { jwtVerify, createRemoteJWKSet, type JWTPayload } from 'jose';
|
||||
import { jwtVerify, createRemoteJWKSet } from 'jose';
|
||||
|
||||
/**
|
||||
* JWT Auth Guard using JWKS (Better Auth compatible)
|
||||
|
|
@ -23,7 +23,10 @@ export class JwtAuthGuard implements CanActivate {
|
|||
const request = context.switchToHttp().getRequest();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
|
||||
console.log('[JwtAuthGuard] Token (first 50 chars):', token?.substring(0, 50));
|
||||
|
||||
if (!token) {
|
||||
console.log('[JwtAuthGuard] No token provided');
|
||||
throw new UnauthorizedException('No token provided');
|
||||
}
|
||||
|
||||
|
|
@ -32,17 +35,22 @@ export class JwtAuthGuard implements CanActivate {
|
|||
if (!this.jwks) {
|
||||
const baseUrl = this.configService.get<string>('BASE_URL') || 'http://localhost:3001';
|
||||
const jwksUrl = new URL('/api/v1/auth/jwks', baseUrl);
|
||||
console.log('[JwtAuthGuard] Initializing JWKS from:', jwksUrl.toString());
|
||||
this.jwks = createRemoteJWKSet(jwksUrl);
|
||||
}
|
||||
|
||||
const issuer = this.configService.get<string>('jwt.issuer') || 'manacore';
|
||||
const audience = this.configService.get<string>('jwt.audience') || 'manacore';
|
||||
|
||||
console.log('[JwtAuthGuard] Verifying with issuer:', issuer, 'audience:', audience);
|
||||
|
||||
const { payload } = await jwtVerify(token, this.jwks, {
|
||||
issuer,
|
||||
audience,
|
||||
});
|
||||
|
||||
console.log('[JwtAuthGuard] Verification SUCCESS, user:', payload.sub);
|
||||
|
||||
// Attach user to request
|
||||
request.user = {
|
||||
userId: payload.sub,
|
||||
|
|
@ -52,7 +60,7 @@ export class JwtAuthGuard implements CanActivate {
|
|||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.debug('[JwtAuthGuard] Token verification failed:', error);
|
||||
console.error('[JwtAuthGuard] Token verification FAILED:', error);
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
222
services/mana-core-auth/src/db/migrate.ts
Normal file
222
services/mana-core-auth/src/db/migrate.ts
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
/**
|
||||
* Database Migration Script with Advisory Locks
|
||||
*
|
||||
* This script safely runs database migrations with the following features:
|
||||
* - Advisory locks to prevent concurrent migrations
|
||||
* - Retry logic for transient network failures
|
||||
* - Timeout protection
|
||||
* - Proper cleanup on exit
|
||||
* - Graceful handling when no migrations exist
|
||||
*
|
||||
* Usage:
|
||||
* pnpm db:migrate # Run migrations
|
||||
* MIGRATION_TIMEOUT=600 pnpm db:migrate # With custom timeout (seconds)
|
||||
*/
|
||||
|
||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import { migrate } from 'drizzle-orm/postgres-js/migrator';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import postgres from 'postgres';
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
// Configuration
|
||||
const MIGRATION_LOCK_ID = 987654321; // Unique lock ID for mana-core-auth migrations
|
||||
const MAX_LOCK_WAIT_MS = parseInt(process.env.MIGRATION_TIMEOUT || '300', 10) * 1000; // Default 5 minutes
|
||||
const MAX_RETRIES = 3;
|
||||
const RETRY_DELAY_MS = 2000;
|
||||
|
||||
/**
|
||||
* Retry wrapper for transient errors
|
||||
*/
|
||||
async function withRetry<T>(
|
||||
operation: () => Promise<T>,
|
||||
operationName: string,
|
||||
maxRetries = MAX_RETRIES
|
||||
): Promise<T> {
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
// Check if error is transient (network-related)
|
||||
const isTransient =
|
||||
lastError.message?.includes('ECONNREFUSED') ||
|
||||
lastError.message?.includes('ETIMEDOUT') ||
|
||||
lastError.message?.includes('ENOTFOUND') ||
|
||||
lastError.message?.includes('connection') ||
|
||||
(lastError as any).code === '57P03'; // PostgreSQL: cannot connect now
|
||||
|
||||
if (!isTransient || attempt === maxRetries) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1); // Exponential backoff
|
||||
console.log(
|
||||
`\u26a0\ufe0f [${operationName}] Transient error, retrying in ${delay}ms... (attempt ${attempt}/${maxRetries})`
|
||||
);
|
||||
console.log(` Error: ${lastError.message}`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire PostgreSQL advisory lock
|
||||
*/
|
||||
async function acquireLock(db: ReturnType<typeof drizzle>): Promise<boolean> {
|
||||
const result = await db.execute(
|
||||
sql`SELECT pg_try_advisory_lock(${MIGRATION_LOCK_ID}) as acquired`
|
||||
);
|
||||
return (result as any)[0]?.acquired === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release PostgreSQL advisory lock
|
||||
*/
|
||||
async function releaseLock(db: ReturnType<typeof drizzle>): Promise<void> {
|
||||
await db.execute(sql`SELECT pg_advisory_unlock(${MIGRATION_LOCK_ID})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for migration lock with timeout
|
||||
*/
|
||||
async function waitForLock(db: ReturnType<typeof drizzle>): Promise<boolean> {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < MAX_LOCK_WAIT_MS) {
|
||||
const acquired = await acquireLock(db);
|
||||
if (acquired) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
||||
console.log(`\u23f3 Waiting for migration lock... (${elapsed}s / ${MAX_LOCK_WAIT_MS / 1000}s)`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main migration function
|
||||
*/
|
||||
async function runMigrations(): Promise<void> {
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
|
||||
if (!databaseUrl) {
|
||||
throw new Error('DATABASE_URL environment variable is not set');
|
||||
}
|
||||
|
||||
console.log('\n\ud83d\udd04 Starting database migration process...');
|
||||
console.log(` Lock ID: ${MIGRATION_LOCK_ID}`);
|
||||
console.log(` Timeout: ${MAX_LOCK_WAIT_MS / 1000}s`);
|
||||
console.log('');
|
||||
|
||||
// Create connection with single connection for migrations
|
||||
const connection = postgres(databaseUrl, {
|
||||
max: 1,
|
||||
idle_timeout: 20,
|
||||
connect_timeout: 30,
|
||||
});
|
||||
|
||||
const db = drizzle(connection);
|
||||
let lockAcquired = false;
|
||||
|
||||
try {
|
||||
// Test database connection
|
||||
console.log('\ud83d\udd0c Testing database connection...');
|
||||
await withRetry(async () => {
|
||||
await db.execute(sql`SELECT 1`);
|
||||
}, 'Database connection');
|
||||
console.log('\u2705 Database connection successful\n');
|
||||
|
||||
// Attempt to acquire advisory lock
|
||||
console.log('\ud83d\udd12 Attempting to acquire migration lock...');
|
||||
|
||||
lockAcquired = await withRetry(() => acquireLock(db), 'Acquire lock');
|
||||
|
||||
if (!lockAcquired) {
|
||||
console.log('\u23f3 Another instance is running migrations. Waiting for lock...');
|
||||
|
||||
lockAcquired = await waitForLock(db);
|
||||
|
||||
if (!lockAcquired) {
|
||||
throw new Error(
|
||||
`Migration lock timeout after ${MAX_LOCK_WAIT_MS / 1000}s - another migration may be stuck`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\u2705 Migration lock acquired\n');
|
||||
|
||||
// Check if migration files exist
|
||||
const migrationsFolder = './src/db/migrations';
|
||||
const journalPath = path.join(migrationsFolder, 'meta', '_journal.json');
|
||||
|
||||
if (!fs.existsSync(journalPath)) {
|
||||
console.log('\u26a0\ufe0f No migration files found (meta/_journal.json missing)');
|
||||
console.log(' This is normal if you have not generated any migrations yet.');
|
||||
console.log(' To generate migrations, run: pnpm db:generate');
|
||||
console.log(' For development, you can use: pnpm db:push');
|
||||
console.log('\n\u2705 No migrations to run\n');
|
||||
return;
|
||||
}
|
||||
|
||||
// Run migrations
|
||||
console.log('\ud83d\udce6 Running database migrations...');
|
||||
|
||||
await withRetry(
|
||||
async () => {
|
||||
await migrate(db, {
|
||||
migrationsFolder,
|
||||
});
|
||||
},
|
||||
'Run migrations',
|
||||
1 // Only 1 attempt for actual migrations (they should be idempotent)
|
||||
);
|
||||
|
||||
console.log('\u2705 Migrations completed successfully\n');
|
||||
} catch (error) {
|
||||
console.error('\n\u274c Migration failed:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
// Always attempt to release lock
|
||||
if (lockAcquired) {
|
||||
try {
|
||||
await releaseLock(db);
|
||||
console.log('\ud83d\udd13 Migration lock released');
|
||||
} catch (unlockError) {
|
||||
console.error('\u26a0\ufe0f Failed to release lock:', unlockError);
|
||||
}
|
||||
}
|
||||
|
||||
// Close connection
|
||||
try {
|
||||
await connection.end();
|
||||
console.log('\ud83d\udd0c Database connection closed\n');
|
||||
} catch (closeError) {
|
||||
console.error('\u26a0\ufe0f Failed to close connection:', closeError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run migrations
|
||||
runMigrations()
|
||||
.then(() => {
|
||||
console.log('\ud83c\udf89 Migration process completed successfully');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n\ud83d\udca5 Migration process failed:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
CREATE SCHEMA "feedback";
|
||||
--> statement-breakpoint
|
||||
CREATE TYPE "public"."feedback_category" AS ENUM('bug', 'feature', 'improvement', 'question', 'other');--> statement-breakpoint
|
||||
CREATE TYPE "public"."feedback_status" AS ENUM('submitted', 'under_review', 'planned', 'in_progress', 'completed', 'declined');--> statement-breakpoint
|
||||
CREATE TABLE "feedback"."feedback_votes" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"feedback_id" uuid NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "feedback"."user_feedback" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"app_id" text NOT NULL,
|
||||
"title" text,
|
||||
"feedback_text" text NOT NULL,
|
||||
"category" "feedback_category" DEFAULT 'feature' NOT NULL,
|
||||
"status" "feedback_status" DEFAULT 'submitted' NOT NULL,
|
||||
"is_public" boolean DEFAULT false NOT NULL,
|
||||
"admin_response" text,
|
||||
"vote_count" integer DEFAULT 0 NOT NULL,
|
||||
"device_info" jsonb,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"published_at" timestamp with time zone,
|
||||
"completed_at" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "feedback"."feedback_votes" ADD CONSTRAINT "feedback_votes_feedback_id_user_feedback_id_fk" FOREIGN KEY ("feedback_id") REFERENCES "feedback"."user_feedback"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "feedback"."feedback_votes" ADD CONSTRAINT "feedback_votes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "feedback"."user_feedback" ADD CONSTRAINT "user_feedback_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "feedback_vote_unique" ON "feedback"."feedback_votes" USING btree ("feedback_id","user_id");--> statement-breakpoint
|
||||
CREATE INDEX "feedback_votes_feedback_idx" ON "feedback"."feedback_votes" USING btree ("feedback_id");--> statement-breakpoint
|
||||
CREATE INDEX "feedback_user_idx" ON "feedback"."user_feedback" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "feedback_app_idx" ON "feedback"."user_feedback" USING btree ("app_id");--> statement-breakpoint
|
||||
CREATE INDEX "feedback_public_idx" ON "feedback"."user_feedback" USING btree ("is_public");--> statement-breakpoint
|
||||
CREATE INDEX "feedback_status_idx" ON "feedback"."user_feedback" USING btree ("status");--> statement-breakpoint
|
||||
CREATE INDEX "feedback_created_at_idx" ON "feedback"."user_feedback" USING btree ("created_at");
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue