45 |
46 |
--------------------------------------------------------------------------------
/studio/patches/04-tweets.patch:
--------------------------------------------------------------------------------
1 | diff --git a/packages/shared-data/tweets.ts b/packages/shared-data/tweets.ts
2 | index 842800d..f6e20a9 100644
3 | --- a/packages/shared-data/tweets.ts
4 | +++ b/packages/shared-data/tweets.ts
5 | @@ -1,231 +1,9 @@
6 | const tweets = [
7 | {
8 | - text: "Working with @supabase has been one of the best dev experiences I've had lately. Incredibly easy to set up, great documentation, and so many fewer hoops to jump through than the competition. I definitely plan to use it on any and all future projects.",
9 | - url: 'https://twitter.com/thatguy_tex/status/1497602628410388480',
10 | - handle: 'thatguy_tex',
11 | - img_url: '/images/twitter-profiles/09HouOSt_400x400.jpg',
12 | - },
13 | - {
14 | - text: '@supabase is just 🤯 Now I see why a lot of people love using it as a backend for their applications. I am really impressed with how easy it is to set up an Auth and then just code it together for the frontend. @IngoKpp now I see your joy with Supabase #coding #fullstackwebdev',
15 | - url: 'https://twitter.com/IxoyeDesign/status/1497473731777728512',
16 | - handle: 'IxoyeDesign',
17 | - img_url: '/images/twitter-profiles/C8opIL-g_400x400.jpg',
18 | - },
19 | - {
20 | - text: "I've been using @supabase for two personal projects and it has been amazing being able to use the power of Postgres and don't have to worry about the backend",
21 | - url: 'https://twitter.com/varlenneto/status/1496595780475535366',
22 | - handle: 'varlenneto',
23 | - img_url: '/images/twitter-profiles/wkXN0t_F_400x400.jpg',
24 | - },
25 | - {
26 | - text: "Y'all @supabase + @nextjs is amazing! 🙌 Barely an hour into a proof-of-concept and already have most of the functionality in place. 🤯🤯🤯",
27 | - url: 'https://twitter.com/justinjunodev/status/1500264302749622273',
28 | - handle: 'justinjunodev',
29 | - img_url: '/images/twitter-profiles/9k_ZB9OO_400x400.jpg',
30 | - },
31 | - {
32 | - text: 'And thanks to @supabase, I was able to go from idea to launched feature in a matter of hours. Absolutely amazing!',
33 | - url: 'https://twitter.com/BraydonCoyer/status/1511071369731137537',
34 | - handle: 'BraydonCoyer',
35 | - img_url: '/images/twitter-profiles/8YxkpW8f_400x400.jpg',
36 | - },
37 | - {
38 | - text: 'Contributing to open-source projects and seeing merged PRs gives enormous happiness! Special thanks to @supabase, for giving this opportunity by staying open-source and being junior-friendly✌🏼',
39 | - url: 'https://twitter.com/damlakoksal/status/1511436907984662539',
40 | - handle: 'damlakoksal',
41 | - img_url: '/images/twitter-profiles/N8EfTFs7_400x400.jpg',
42 | - },
43 | - {
44 | - text: "Holy crap. @supabase is absolutely incredible. Most elegant backend as a service I've ever used. This is a dream.",
45 | - url: 'https://twitter.com/kentherogers/status/1512609587110719488',
46 | - handle: 'KenTheRogers',
47 | - img_url: '/images/twitter-profiles/9l9Td-Fz_400x400.jpg',
48 | - },
49 | - {
50 | - text: "Over the course of a few weeks, we migrated 125.000 users (email/pw, Gmail, Facebook, Apple logins) from Auth0 to @supabase and have now completed the migration. I'm just glad the migration is done 😅 Went well, besides a few edge cases (duplicate emails/linked accounts)",
51 | - url: 'https://twitter.com/kevcodez/status/1518548401587204096',
52 | - handle: 'kevcodez',
53 | - img_url: '/images/twitter-profiles/t6lpcRcn_400x400.jpg',
54 | - },
55 | - {
56 | - text: "Using @supabase I'm really pleased on the power of postgres (and sql in general). Despite being a bit dubious about the whole backend as a service thing I have to say I really don't miss anything. The whole experience feel very robust and secure.",
57 | - url: 'https://twitter.com/paoloricciuti/status/1497691838597066752',
58 | - handle: 'PaoloRicciuti',
59 | - img_url: '/images/twitter-profiles/OCDKFUOp_400x400.jpg',
60 | - },
61 | - {
62 | - text: '@supabase is lit. It took me less than 10 minutes to setup, the DX is just amazing.',
63 | - url: 'https://twitter.com/saxxone/status/1500812171063828486',
64 | - handle: 'saxxone',
65 | - img_url: '/images/twitter-profiles/BXi6z1M7_400x400.jpg',
66 | - },
67 | - {
68 | - text: 'I’m not sure what magic @supabase is using but we’ve migrated @happyteamsdotio database to @supabase from @heroku and it’s much much faster at half the cost.',
69 | - url: 'https://twitter.com/michaelcdever/status/1524753565599690754',
70 | - handle: 'michaelcdever',
71 | - img_url: '/images/twitter-profiles/rWX8Jzp5_400x400.jpg',
72 | - },
73 | - {
74 | - text: 'There are a lot of indie hackers building in public, but it’s rare to see a startup shipping as consistently and transparently as Supabase. Their upcoming March releases look to be 🔥 Def worth a follow! also opened my eyes as to how to value add in open source.',
75 | - url: 'https://twitter.com/swyx/status/1366685025047994373',
76 | - handle: 'swyx',
77 | - img_url: '/images/twitter-profiles/qhvO9V6x_400x400.jpg',
78 | - },
79 | - {
80 | - text: 'This weekend I made a personal record 🥇 on the less time spent creating an application with social login / permissions, database, cdn, infinite scaling, git push to deploy and for free. Thanks to @supabase and @vercel',
81 | - url: 'https://twitter.com/jperelli/status/1366195769657720834',
82 | - handle: 'jperelli',
83 | - img_url: '/images/twitter-profiles/_ki30kYo_400x400.jpg',
84 | - },
85 | - {
86 | - text: 'Badass! Supabase is amazing. literally saves our small team a whole engineer’s worth of work constantly. The founders and everyone I’ve chatted with at supabase are just awesome people as well :)',
87 | - url: 'https://twitter.com/KennethCassel/status/1524359528619384834',
88 | - handle: 'KennethCassel',
89 | - img_url: '/images/twitter-profiles/pmQj3TX-_400x400.jpg',
90 | - },
91 | - {
92 | - text: 'Working with Supabase is just fun. It makes working with a DB so much easier.',
93 | - url: 'https://twitter.com/the_BrianB/status/1524716498442276864',
94 | - handle: 'the_BrianB',
95 | - img_url: '/images/twitter-profiles/7NITI8Z3_400x400.jpg',
96 | - },
97 | - {
98 | - text: 'This community is STRONG and will continue to be the reason why developers flock to @supabase over an alternative. Keep up the good work! ⚡️',
99 | - url: 'https://twitter.com/_wilhelm__/status/1524074865107488769',
100 | - handle: '_wilhelm__',
101 | - img_url: '/images/twitter-profiles/CvqDy6YF_400x400.jpg',
102 | - },
103 | - {
104 | - text: "Working on my next SaaS app and I want this to be my whole job because I'm just straight out vibing putting it together. @supabase and chill, if you will",
105 | - url: 'https://twitter.com/drewclemcr8/status/1523843155484942340',
106 | - handle: 'drewclemcr8',
107 | - img_url: '/images/twitter-profiles/bJlKtSxz_400x400.jpg',
108 | - },
109 | - {
110 | - text: '@supabase Putting a ton of well-explained example API queries in a self-building documentation is just a classy move all around. I also love having GraphQL-style nested queries with traditional SQL filtering. This is pure DX delight. A+++. #backend',
111 | - url: 'https://twitter.com/CodiferousCoder/status/1522233113207836675',
112 | - handle: 'CodiferousCoder',
113 | - img_url: '/images/twitter-profiles/t37cVLwy_400x400.jpg',
114 | - },
115 | - {
116 | - text: 'Me using @supabase for the first time right now 🤯',
117 | - url: 'https://twitter.com/nasiscoe/status/1365140856035024902',
118 | - handle: 'nasiscoe',
119 | - img_url: '/images/twitter-profiles/nc2Ms5hH_400x400.jpg',
120 | - },
121 | - {
122 | - text: "I'm trying @supabase, Firebase alternative that uses PostgreSQL (and you can use GraphQL too) in the cloud. It's incredible 😍",
123 | - url: 'https://twitter.com/JP__Gallegos/status/1365699468109242374',
124 | - handle: 'JP__Gallegos',
125 | - img_url: '/images/twitter-profiles/1PH2mt6v_400x400.jpg',
126 | - },
127 | - {
128 | - text: 'Check out this amazing product @supabase. A must give try #newidea #opportunity',
129 | - url: 'https://twitter.com/digitaldaswani/status/1364447219642814464',
130 | - handle: 'digitaldaswani',
131 | - img_url: '/images/twitter-profiles/w8HLdlC7_400x400.jpg',
132 | - },
133 | - {
134 | - text: "I gave @supabase a try this weekend and I was able to create a quick dashboard to visualize the data from the PostgreSQL instance. It's super easy to use Supabase's API or the direct DB connection. Check out the tutorial 📖",
135 | - url: 'https://twitter.com/razvanilin/status/1363770020581412867',
136 | - handle: 'razvanilin',
137 | - img_url: '/images/twitter-profiles/AiaH9vJ2_400x400.jpg',
138 | - },
139 | - {
140 | - text: "Tried @supabase for the first time yesterday. Amazing tool! I was able to get my Posgres DB up in no time and their documentation on operating on the DB is super easy! 👏 Can't wait for Cloud functions to arrive! It's gonna be a great Firebase alternative!",
141 | - url: 'https://twitter.com/chinchang457/status/1363347740793524227',
142 | - handle: 'chinchang457',
143 | - img_url: '/images/twitter-profiles/LTw5OCnv_400x400.jpg',
144 | - },
145 | - {
146 | - text: '10/100 All day i was migrating my project from firebase to @supabase Because it is perfect and simple!!! I like design and API for understandable. There are in BETA now. Just try!🧪',
147 | - url: 'https://twitter.com/roomahhka/status/1363155396391763971',
148 | - handle: 'roomahhka',
149 | - img_url: '/images/twitter-profiles/e_2eQt6C_400x400.jpg',
150 | - },
151 | - {
152 | - text: 'I gave @supabase a try today and I was positively impressed! Very quick setup to get a working remote database with API access and documentation generated automatically for you 👌 10/10 will play more',
153 | - url: 'https://twitter.com/razvanilin/status/1363002398738800640',
154 | - handle: 'razvanilin',
155 | - img_url: '/images/twitter-profiles/AiaH9vJ2_400x400.jpg',
156 | - },
157 | - {
158 | - text: "Wait. Is it so easy to write queries for @supabase ? It's like simple SQL stuff!",
159 | - url: 'https://twitter.com/T0ny_Boy/status/1362911838908911617',
160 | - handle: 'T0ny_Boy',
161 | - img_url: '/images/twitter-profiles/UCBhUBZl_400x400.jpg',
162 | - },
163 | - {
164 | - text: 'Jeez, and @supabase have native support for magic link login?! I was going to use http://magic.link for this But if I can get my whole DB + auth + magic link support in one... Awesome',
165 | - url: 'https://twitter.com/louisbarclay/status/1362016666868154371',
166 | - handle: 'louisbarclay',
167 | - img_url: '/images/twitter-profiles/6f1O8ZOW_400x400.jpg',
168 | - },
169 | - {
170 | - text: 'Where has @supabase been all my life? 😍',
171 | - url: 'https://twitter.com/Elsolo244/status/1360257201911320579',
172 | - handle: 'Elsolo244',
173 | - img_url: '/images/twitter-profiles/v6citnk33y2wpeyzrq05_400x400.jpeg',
174 | - },
175 | - {
176 | - text: 'Honestly Supabase is such a killer Firebase alternative.',
177 | - url: 'https://twitter.com/XPCheese/status/1360229397735895043',
178 | - handle: 'XPCheese',
179 | - img_url: '/images/twitter-profiles/eYP6YXr7_400x400.jpg',
180 | - },
181 | - {
182 | - text: "I think you'll love @supabase :-) Open-source, PostgreSQL-based & zero magic.",
183 | - url: 'https://twitter.com/zippoxer/status/1360021315852328961',
184 | - handle: 'zippoxer',
185 | - img_url: '/images/twitter-profiles/6rd3xub9_400x400.png',
186 | - },
187 | - {
188 | - text: '@supabase is insane.',
189 | - url: 'https://twitter.com/codewithbhargav/status/1357647840911126528',
190 | - handle: 'codewithbhargav',
191 | - img_url: '/images/twitter-profiles/LQYfHXBp_400x400.jpg',
192 | - },
193 | - {
194 | - text: 'It’s fun, feels lightweight, and really quick to spin up user auth and a few tables. Almost too easy! Highly recommend.',
195 | - url: 'https://twitter.com/nerdburn/status/1356857261495214085',
196 | - handle: 'nerdburn',
197 | - img_url: '/images/twitter-profiles/66VSV9Mm_400x400.png',
198 | - },
199 | - {
200 | - text: 'Now things are starting to get interesting! Firebase has long been the obvious choice for many #flutter devs for the ease of use. But their databases are NoSQL, which has its downsides... Seems like @supabase is working on something interesting here!',
201 | - url: 'https://twitter.com/RobertBrunhage/status/1356973695865085953',
202 | - handle: 'RobertBrunhage',
203 | - img_url: '/images/twitter-profiles/5LMWEACf_400x400.jpg',
204 | - },
205 | - {
206 | - text: "Honestly, I really love what @supabase is doing, you don't need to own a complete backend, just write your logic within your app and you'll get a powerful Postgresql at your disposal.",
207 | - url: 'https://twitter.com/NavicsteinR/status/1356927229217959941',
208 | - handle: 'NavicsteinR',
209 | - img_url: '/images/twitter-profiles/w_zNZAs7_400x400.jpg',
210 | - },
211 | - {
212 | - text: "I've really enjoyed the DX! Extremely fun to use, which is odd to say about a database at least for me.",
213 | - url: 'https://twitter.com/Soham_Asmi/status/1373086068132745217',
214 | - handle: 'Soham_Asmi',
215 | - img_url: '/images/twitter-profiles/Os4nhKIr_400x400.jpg',
216 | - },
217 | - {
218 | - text: 'Supabase team is doing some awesome stuff #supabase #facts @supabase',
219 | - url: 'https://twitter.com/_strawbird/status/1372607500499841025',
220 | - handle: '_strawbird',
221 | - img_url: '/images/twitter-profiles/iMBvvQdn_400x400.jpg',
222 | - },
223 | - {
224 | - text: "Did a website with @supabase last week with no prior experience with it. Up and running in 20 minutes. It's awesome to use. Thumbs up",
225 | - url: 'https://twitter.com/michael_webdev/status/1352885366928404481?s=20',
226 | - handle: 'michael_webdev',
227 | - img_url: '/images/twitter-profiles/SvAyLaWV_400x400.jpg',
228 | - },
229 | - {
230 | - text: 'I just learned about @supabase and im in love 😍 Supabase is an open source Firebase alternative! EarListen (& react) to database changes 💁 Manage users & permissions 🔧 Simple UI for database interaction',
231 | - url: 'https://twitter.com/0xBanana/status/1373677301905362948',
232 | - handle: '0xBanana',
233 | - img_url: '/images/twitter-profiles/pgHIGqZ0_400x400.jpg',
234 | + text: "Self-hosted version of @supabase, powered by SupaManager.",
235 | + url: 'https://twitter.com/theharryet',
236 | + handle: 'theharryet',
237 | + img_url: '/img/twitter-profiles/MpAMDXmc_400x400.jpg',
238 | },
239 | ]
240 |
241 |
--------------------------------------------------------------------------------
/studio/patches/05-fixpatches.patch:
--------------------------------------------------------------------------------
1 | diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx
2 | index 64d9fde..899d86c 100644
3 | --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx
4 | +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx
5 | @@ -112,10 +112,7 @@ const DeployNewReplicaPanel = ({
6 | const [selectedCompute, setSelectedCompute] = useState(defaultCompute)
7 | const selectedComputeMeta = computeAddons.find((addon) => addon.identifier === selectedCompute)
8 |
9 | - const availableRegions =
10 | - process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
11 | - ? AVAILABLE_REPLICA_REGIONS.filter((x) => x.key === 'SOUTHEAST_ASIA')
12 | - : AVAILABLE_REPLICA_REGIONS
13 | + const availableRegions = AVAILABLE_REPLICA_REGIONS
14 |
15 | const onSubmit = async () => {
16 | const regionKey = K8S_REGIONS_VALUES[selectedRegion]
17 |
--------------------------------------------------------------------------------
/studio/patches/06-uifixes.patch:
--------------------------------------------------------------------------------
1 | diff --git a/apps/studio/components/layouts/SignInLayout/SignInLayout.tsx b/apps/studio/components/layouts/SignInLayout/SignInLayout.tsx
2 | index c1b2b53..df65ada 100644
3 | --- a/apps/studio/components/layouts/SignInLayout/SignInLayout.tsx
4 | +++ b/apps/studio/components/layouts/SignInLayout/SignInLayout.tsx
5 | @@ -70,7 +70,7 @@ const SignInLayout = ({
6 | return
7 | }
8 | })
9 | - .catch(() => {}) // catch all errors thrown by auth methods
10 | + .catch(() => { }) // catch all errors thrown by auth methods
11 | }, [])
12 |
13 | const [quote, setQuote] = useState<{
14 | @@ -90,9 +90,8 @@ const SignInLayout = ({
15 | <>
16 |
17 |
24 |
25 |
26 | @@ -172,7 +171,7 @@ const SignInLayout = ({
27 | className="flex items-center gap-4"
28 | >
29 |
35 | diff --git a/apps/studio/pages/sign-in.tsx b/apps/studio/pages/sign-in.tsx
36 | index 4c94b82..72aafc6 100644
37 | --- a/apps/studio/pages/sign-in.tsx
38 | +++ b/apps/studio/pages/sign-in.tsx
39 | @@ -11,15 +11,6 @@ const SignInPage: NextPageWithLayout = () => {
40 | return (
41 | <>
42 |
43 | -
44 | -
47 | -
48 | - or
49 | -
50 | -
51 | -
52 |
53 |
54 |
55 | diff --git a/apps/studio/pages/sign-up.tsx b/apps/studio/pages/sign-up.tsx
56 | index c243efa..fe35e03 100644
57 | --- a/apps/studio/pages/sign-up.tsx
58 | +++ b/apps/studio/pages/sign-up.tsx
59 | @@ -7,15 +7,6 @@ const SignUpPage: NextPageWithLayout = () => {
60 | return (
61 | <>
62 |
63 | -
64 | -
67 | -
68 | - or
69 | -
70 | -
71 | -
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/studio/patches/07-hcaptcha.patch:
--------------------------------------------------------------------------------
1 | diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx
2 | index 7cb44fc..335a3d2 100644
3 | --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx
4 | +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx
5 | @@ -53,8 +53,12 @@ const ExitSurveyModal = ({ visible, subscription, onClose }: ExitSurveyModalProp
6 | }
7 |
8 | const resetCaptcha = () => {
9 | - setCaptchaToken(null)
10 | - captchaRef.current?.resetCaptcha()
11 | + try {
12 | + setCaptchaToken(null)
13 | + captchaRef.current?.resetCaptcha()
14 | + } catch (e) {
15 | + console.warn('Failed to reset captcha', e);
16 | + }
17 | }
18 |
19 | const onSubmit = async () => {
20 | @@ -63,12 +67,17 @@ const ExitSurveyModal = ({ visible, subscription, onClose }: ExitSurveyModalProp
21 | }
22 |
23 | let token = captchaToken
24 | -
25 | - if (!token) {
26 | - const captchaResponse = await captchaRef.current?.execute({ async: true })
27 | - token = captchaResponse?.response ?? null
28 | - await downgradeOrganization()
29 | + try {
30 | + if (!token) {
31 | + const captchaResponse = await captchaRef.current?.execute({ async: true })
32 | + token = captchaResponse?.response ?? null
33 | + }
34 | + } catch (e) {
35 | + console.warn('Failed to execute captcha, continuing without captcha', e);
36 | + token = null
37 | }
38 | +
39 | + await downgradeOrganization()
40 | }
41 |
42 | const downgradeOrganization = async () => {
43 | @@ -146,14 +155,13 @@ const ExitSurveyModal = ({ visible, subscription, onClose }: ExitSurveyModalProp
44 |
60 | 1 ? 's' : ''
67 | - } will be restarted upon hitting confirm`}
68 | + title={`${projectsWithComputeInstances.length} of your project${projectsWithComputeInstances.length > 1 ? 's' : ''
69 | + } will be restarted upon hitting confirm`}
70 | >
71 | This is due to changes in compute instances from the downgrade. Affected project(s)
72 | include {projectsWithComputeInstances.map((project) => project.name).join(', ')}.
73 | diff --git a/apps/studio/components/interfaces/SignIn/ForgotPasswordForm.tsx b/apps/studio/components/interfaces/SignIn/ForgotPasswordForm.tsx
74 | index 0c5630b..0ee1669 100644
75 | --- a/apps/studio/components/interfaces/SignIn/ForgotPasswordForm.tsx
76 | +++ b/apps/studio/components/interfaces/SignIn/ForgotPasswordForm.tsx
77 | @@ -25,27 +25,35 @@ const ForgotPasswordForm = () => {
78 | await router.push('/sign-in')
79 | },
80 | onError: (error) => {
81 | - setCaptchaToken(null)
82 | - captchaRef.current?.resetCaptcha()
83 | + try {
84 | + setCaptchaToken(null)
85 | + captchaRef.current?.resetCaptcha()
86 | + } catch (e) {
87 | + console.warn('Failed to reset captcha', e);
88 | + }
89 | toast.error(`Failed to send reset email: ${error.message}`)
90 | },
91 | })
92 |
93 | const onForgotPassword = async ({ email }: { email: string }) => {
94 | let token = captchaToken
95 | - if (!token) {
96 | - const captchaResponse = await captchaRef.current?.execute({ async: true })
97 | - token = captchaResponse?.response ?? null
98 | + try {
99 | + if (!token) {
100 | + const captchaResponse = await captchaRef.current?.execute({ async: true })
101 | + token = captchaResponse?.response ?? null
102 | + }
103 | + } catch (e) {
104 | + console.warn('Failed to execute captcha, continuing without captcha', e);
105 | + token = null
106 | }
107 |
108 | resetPassword({
109 | email,
110 | hcaptchaToken: token,
111 | - redirectTo: `${
112 | - process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
113 | - ? location.origin
114 | - : process.env.NEXT_PUBLIC_SITE_URL
115 | - }${BASE_PATH}/reset-password`,
116 | + redirectTo: `${process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
117 | + ? location.origin
118 | + : process.env.NEXT_PUBLIC_SITE_URL
119 | + }${BASE_PATH}/reset-password`,
120 | })
121 | }
122 |
123 | diff --git a/apps/studio/components/interfaces/SignIn/SignInForm.tsx b/apps/studio/components/interfaces/SignIn/SignInForm.tsx
124 | index f0d5bf5..aaec44e 100644
125 | --- a/apps/studio/components/interfaces/SignIn/SignInForm.tsx
126 | +++ b/apps/studio/components/interfaces/SignIn/SignInForm.tsx
127 | @@ -27,9 +27,14 @@ const SignInForm = () => {
128 | const toastId = toast.loading('Signing in...')
129 |
130 | let token = captchaToken
131 | - if (!token) {
132 | - const captchaResponse = await captchaRef.current?.execute({ async: true })
133 | - token = captchaResponse?.response ?? null
134 | + try {
135 | + if (!token) {
136 | + const captchaResponse = await captchaRef.current?.execute({ async: true })
137 | + token = captchaResponse?.response ?? null
138 | + }
139 | + } catch (e) {
140 | + console.warn('Failed to execute captcha, continuing without captcha', e);
141 | + token = null
142 | }
143 |
144 | const { error } = await auth.signInWithPassword({
145 | @@ -59,8 +64,12 @@ const SignInForm = () => {
146 | toast.error((error as AuthError).message, { id: toastId })
147 | }
148 | } else {
149 | - setCaptchaToken(null)
150 | - captchaRef.current?.resetCaptcha()
151 | + try {
152 | + setCaptchaToken(null)
153 | + captchaRef.current?.resetCaptcha()
154 | + } catch (e) {
155 | + console.warn('Failed to reset captcha', e);
156 | + }
157 |
158 | if (error.message.toLowerCase() === 'email not confirmed') {
159 | return toast.error(
160 | diff --git a/apps/studio/components/interfaces/SignIn/SignInSSOForm.tsx b/apps/studio/components/interfaces/SignIn/SignInSSOForm.tsx
161 | index 6708879..137c74b 100644
162 | --- a/apps/studio/components/interfaces/SignIn/SignInSSOForm.tsx
163 | +++ b/apps/studio/components/interfaces/SignIn/SignInSSOForm.tsx
164 | @@ -22,17 +22,21 @@ const SignInSSOForm = () => {
165 | const toastId = toast.loading('Signing in...')
166 |
167 | let token = captchaToken
168 | - if (!token) {
169 | - const captchaResponse = await captchaRef.current?.execute({ async: true })
170 | - token = captchaResponse?.response ?? null
171 | + try {
172 | + if (!token) {
173 | + const captchaResponse = await captchaRef.current?.execute({ async: true })
174 | + token = captchaResponse?.response ?? null
175 | + }
176 | + } catch (e) {
177 | + console.warn('Failed to execute captcha, continuing without captcha', e);
178 | + token = null
179 | }
180 |
181 | // redirects to /sign-in to check if the user has MFA setup (handled in SignInLayout.tsx)
182 | const redirectTo = buildPathWithParams(
183 | - `${
184 | - process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
185 | - ? location.origin
186 | - : process.env.NEXT_PUBLIC_SITE_URL
187 | + `${process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
188 | + ? location.origin
189 | + : process.env.NEXT_PUBLIC_SITE_URL
190 | }${BASE_PATH}/sign-in-mfa`
191 | )
192 |
193 | @@ -52,8 +56,12 @@ const SignInSSOForm = () => {
194 | window.location.href = data.url
195 | }
196 | } else {
197 | - setCaptchaToken(null)
198 | - captchaRef.current?.resetCaptcha()
199 | + try {
200 | + setCaptchaToken(null)
201 | + captchaRef.current?.resetCaptcha()
202 | + } catch (e) {
203 | + console.warn('Failed to reset captcha', e);
204 | + }
205 | toast.error(error.message, { id: toastId })
206 | }
207 | }
208 | diff --git a/apps/studio/components/interfaces/SignIn/SignUpForm.tsx b/apps/studio/components/interfaces/SignIn/SignUpForm.tsx
209 | index 10856dd..90ceeb2 100644
210 | --- a/apps/studio/components/interfaces/SignIn/SignUpForm.tsx
211 | +++ b/apps/studio/components/interfaces/SignIn/SignUpForm.tsx
212 | @@ -26,37 +26,44 @@ const SignUpForm = () => {
213 | setIsSubmitted(true)
214 | },
215 | onError: (error) => {
216 | - setCaptchaToken(null)
217 | - captchaRef.current?.resetCaptcha()
218 | + try {
219 | + setCaptchaToken(null)
220 | + captchaRef.current?.resetCaptcha()
221 | + } catch (e) {
222 | + console.warn('Failed to reset captcha', e);
223 | + }
224 | toast.error(`Failed to sign up: ${error.message}`)
225 | },
226 | })
227 |
228 | const onSignUp = async ({ email, password }: { email: string; password: string }) => {
229 | let token = captchaToken
230 | - if (!token) {
231 | - const captchaResponse = await captchaRef.current?.execute({ async: true })
232 | - token = captchaResponse?.response ?? null
233 | + try {
234 | + if (!token) {
235 | + const captchaResponse = await captchaRef.current?.execute({ async: true })
236 | + token = captchaResponse?.response ?? null
237 | + }
238 | + } catch (e) {
239 | + console.warn('Failed to execute captcha, continuing without captcha', e);
240 | + token = null
241 | }
242 |
243 | signup({
244 | email,
245 | password,
246 | hcaptchaToken: token ?? null,
247 | - redirectTo: `${
248 | - process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
249 | - ? location.origin
250 | - : process.env.NEXT_PUBLIC_SITE_URL
251 | - }${BASE_PATH}/sign-in`,
252 | + redirectTo: `${process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
253 | + ? location.origin
254 | + : process.env.NEXT_PUBLIC_SITE_URL
255 | + }${BASE_PATH}/sign-in`,
256 | })
257 | }
258 |
259 | return (
260 |
261 |
268 |
269 | You've successfully signed up. Please check your email to confirm your account before
270 | @@ -66,9 +73,8 @@ const SignUpForm = () => {
271 |
116 | - }
117 | - type="select"
118 | - value={instanceSize}
119 | - onChange={(value) => setInstanceSize(value)}
120 | - descriptionText={
121 | - <>
122 | -
123 | - Select the size for your dedicated database. You can always change this
124 | - later.
125 | -
126 | -
127 | - Your organization has $10/month in Compute Credits to cover one instance
128 | - on Micro Compute or parts of any other instance size.
129 | -
130 | - >
131 | - }
132 | - >
133 | - {sizes.map((option) => {
134 | - return (
135 | -
140 | -
141 | -
142 | -
146 | - {instanceSizeSpecs[option].label}
147 | -
148 | -
149 | -
150 | -
151 | - {instanceSizeSpecs[option].ram} RAM /{' '}
152 | - {instanceSizeSpecs[option].cpu} CPU
153 | -
154 | -
155 | - {instanceSizeSpecs[option].price}
156 | -
157 | -
158 | -
159 | -
160 | - )
161 | - })}
162 | -
163 | -
164 | - )}
165 | -
166 |
167 | onSelectRegion(value)}
9 | - descriptionText="Select the region closest to your users for the best performance."
10 | + descriptionText="Currently the region selected has no impact."
11 | >
12 |
13 | Select a region for your project
14 | diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts
15 | index cb4b6e4..b91a197 100644
16 | --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts
17 | +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts
18 | @@ -23,6 +23,7 @@ export const REPLICA_STATUS: {
19 |
20 | export const K8S_REGIONS_VALUES: { [key: string]: string } = {
21 | MARS: 'MARS-1',
22 | + JUPITER: 'JUPITER-1',
23 | }
24 |
25 | export const AVAILABLE_REPLICA_REGIONS: Region[] = []
26 | diff --git a/apps/studio/lib/constants/infrastructure.ts b/apps/studio/lib/constants/infrastructure.ts
27 | index 8dbcce7..11c4817 100644
28 | --- a/apps/studio/lib/constants/infrastructure.ts
29 | +++ b/apps/studio/lib/constants/infrastructure.ts
30 | @@ -5,6 +5,7 @@ export type Region = typeof K8S_REGIONS
31 |
32 | export const K8S_REGIONS = {
33 | MARS: 'Mars',
34 | + JUPITER: 'Jupiter',
35 | } as const
36 |
37 | export type K8S_REGIONS_KEYS = keyof typeof K8S_REGIONS
38 |
--------------------------------------------------------------------------------
/studio/patches/14-juststoptelemetry.patch:
--------------------------------------------------------------------------------
1 | diff --git a/apps/studio/components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx b/apps/studio/components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx
2 | index 2fd7bf4..33ca0fb 100644
3 | --- a/apps/studio/components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx
4 | +++ b/apps/studio/components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx
5 | @@ -1,6 +1,4 @@
6 | import HCaptcha from '@hcaptcha/react-hcaptcha'
7 | -import { Elements } from '@stripe/react-stripe-js'
8 | -import { loadStripe } from '@stripe/stripe-js'
9 | import { useTheme } from 'next-themes'
10 | import { useCallback, useEffect, useState } from 'react'
11 | import toast from 'react-hot-toast'
12 | @@ -8,9 +6,7 @@ import { Modal } from 'ui'
13 |
14 | import { useOrganizationPaymentMethodSetupIntent } from 'data/organizations/organization-payment-method-setup-intent-mutation'
15 | import { useSelectedOrganization } from 'hooks'
16 | -import { STRIPE_PUBLIC_KEY } from 'lib/constants'
17 | import { useIsHCaptchaLoaded } from 'stores/hcaptcha-loaded-store'
18 | -import AddPaymentMethodForm from './AddPaymentMethodForm'
19 |
20 | interface AddNewPaymentMethodModalProps {
21 | visible: boolean
22 | @@ -19,8 +15,6 @@ interface AddNewPaymentMethodModalProps {
23 | onConfirm: () => void
24 | }
25 |
26 | -const stripePromise = loadStripe(STRIPE_PUBLIC_KEY)
27 | -
28 | const AddNewPaymentMethodModal = ({
29 | visible,
30 | returnUrl,
31 | @@ -132,15 +126,7 @@ const AddNewPaymentMethodModal = ({
32 | onCancel={onLocalCancel}
33 | className="PAYMENT"
34 | >
35 | -
36 | -
37 | -
42 | -
43 | -
44 | +
45 |
46 | >
47 | )
48 | diff --git a/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx b/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx
49 | index 581fe0a..4c71bfb 100644
50 | --- a/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx
51 | +++ b/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx
52 | @@ -1,6 +1,4 @@
53 | -import { PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js'
54 | import { useState } from 'react'
55 | -import toast from 'react-hot-toast'
56 | import { Button, Modal } from 'ui'
57 |
58 | interface AddPaymentMethodFormProps {
59 | @@ -14,19 +12,11 @@ interface AddPaymentMethodFormProps {
60 | // Small UX annoyance here, that the page will be refreshed
61 |
62 | const AddPaymentMethodForm = ({ returnUrl, onCancel, onConfirm }: AddPaymentMethodFormProps) => {
63 | - const stripe = useStripe()
64 | - const elements = useElements()
65 | -
66 | const [isSaving, setIsSaving] = useState(false)
67 |
68 | const handleSubmit = async (event: any) => {
69 | event.preventDefault()
70 |
71 | - if (!stripe || !elements) {
72 | - console.error('Stripe.js has not loaded')
73 | - return
74 | - }
75 | -
76 | setIsSaving(true)
77 |
78 | if (document !== undefined) {
79 | @@ -34,19 +24,8 @@ const AddPaymentMethodForm = ({ returnUrl, onCancel, onConfirm }: AddPaymentMeth
80 | document.body.classList.add('!pointer-events-auto')
81 | }
82 |
83 | - const { error } = await stripe.confirmSetup({
84 | - elements,
85 | - redirect: 'if_required',
86 | - confirmParams: { return_url: returnUrl },
87 | - })
88 | -
89 | - if (error) {
90 | - setIsSaving(false)
91 | - toast.error(error?.message ?? ' Failed to save card details')
92 | - } else {
93 | - setIsSaving(false)
94 | - onConfirm()
95 | - }
96 | + setIsSaving(false)
97 | + onConfirm()
98 |
99 | if (document !== undefined) {
100 | document.body.classList.remove('!pointer-events-auto')
101 | @@ -58,9 +37,7 @@ const AddPaymentMethodForm = ({ returnUrl, onCancel, onConfirm }: AddPaymentMeth
102 |
103 |
108 | + >
109 |
110 |
111 |
112 | diff --git a/apps/studio/components/ui/PageTelemetry.tsx b/apps/studio/components/ui/PageTelemetry.tsx
113 | index 4381829..f4683b8 100644
114 | --- a/apps/studio/components/ui/PageTelemetry.tsx
115 | +++ b/apps/studio/components/ui/PageTelemetry.tsx
116 | @@ -59,26 +59,27 @@ const PageTelemetry = ({ children }: PropsWithChildren<{}>) => {
117 | /**
118 | * Send page telemetry
119 | */
120 | - post(`${API_URL}/telemetry/page`, {
121 | - referrer: referrer,
122 | - title: document.title,
123 | - route,
124 | - ga: {
125 | - screen_resolution: telemetryProps?.screenResolution,
126 | - language: telemetryProps?.language,
127 | - },
128 | - })
129 | + // Note (Harry): We don't want telemetry! Save the bandwidth :)
130 | + // post(`${API_URL}/telemetry/page`, {
131 | + // referrer: referrer,
132 | + // title: document.title,
133 | + // route,
134 | + // ga: {
135 | + // screen_resolution: telemetryProps?.screenResolution,
136 | + // language: telemetryProps?.language,
137 | + // },
138 | + // })
139 |
140 | - if (isLoggedIn) {
141 | - post(`${API_URL}/telemetry/pageview`, {
142 | - ...(ref && { projectRef: ref }),
143 | - ...(selectedOrganization && { orgSlug: selectedOrganization.slug }),
144 | - referrer: referrer,
145 | - title: document.title,
146 | - path: router.route,
147 | - location: router.asPath,
148 | - })
149 | - }
150 | + // if (isLoggedIn) {
151 | + // post(`${API_URL}/telemetry/pageview`, {
152 | + // ...(ref && { projectRef: ref }),
153 | + // ...(selectedOrganization && { orgSlug: selectedOrganization.slug }),
154 | + // referrer: referrer,
155 | + // title: document.title,
156 | + // path: router.route,
157 | + // location: router.asPath,
158 | + // })
159 | + // }
160 | }
161 |
162 | return <>{children}>
163 | diff --git a/apps/studio/lib/telemetry.ts b/apps/studio/lib/telemetry.ts
164 | index 1726911..8d5ca0e 100644
165 | --- a/apps/studio/lib/telemetry.ts
166 | +++ b/apps/studio/lib/telemetry.ts
167 | @@ -62,13 +62,15 @@ const sendIdentify = (user: User, gaProps?: TelemetryProps) => {
168 | : null
169 | if (consent !== 'true') return
170 |
171 | - return post(`${API_URL}/telemetry/identify`, {
172 | - user,
173 | - ga: {
174 | - screen_resolution: gaProps?.screenResolution,
175 | - language: gaProps?.language,
176 | - },
177 | - })
178 | + return null
179 | +
180 | + // return post(`${API_URL}/telemetry/identify`, {
181 | + // user,
182 | + // ga: {
183 | + // screen_resolution: gaProps?.screenResolution,
184 | + // language: gaProps?.language,
185 | + // },
186 | + // })
187 | }
188 |
189 | /**
190 | @@ -105,7 +107,8 @@ const sendActivity = (
191 | ...(projectRef && { projectRef }),
192 | ...(orgSlug && { orgSlug }),
193 | }
194 | - return post(`${API_URL}/telemetry/activity`, properties)
195 | + return null
196 | + //return post(`${API_URL}/telemetry/activity`, properties)
197 | }
198 |
199 | const Telemetry = {
200 | diff --git a/apps/studio/package.json b/apps/studio/package.json
201 | index 97883b3..29779f9 100644
202 | --- a/apps/studio/package.json
203 | +++ b/apps/studio/package.json
204 | @@ -29,8 +29,6 @@
205 | "@radix-ui/react-tooltip": "^1.0.7",
206 | "@scaleleap/pg-format": "^1.0.0",
207 | "@sentry/nextjs": "^7.108.0",
208 | - "@stripe/react-stripe-js": "^2.5.0",
209 | - "@stripe/stripe-js": "^3.0.5",
210 | "@supabase/auth-helpers-react": "^0.4.2",
211 | "@supabase/pg-meta": "*",
212 | "@supabase/shared-types": "0.1.55",
213 | diff --git a/apps/studio/pages/new/index.tsx b/apps/studio/pages/new/index.tsx
214 | index 821314e..87f0977 100644
215 | --- a/apps/studio/pages/new/index.tsx
216 | +++ b/apps/studio/pages/new/index.tsx
217 | @@ -1,49 +1,24 @@
218 | import HCaptcha from '@hcaptcha/react-hcaptcha'
219 | -import { Elements } from '@stripe/react-stripe-js'
220 | -import { loadStripe } from '@stripe/stripe-js'
221 | -import { useTheme } from 'next-themes'
222 | -import { useCallback, useEffect, useState } from 'react'
223 | +import { useCallback, useState } from 'react'
224 |
225 | import { NewOrgForm } from 'components/interfaces/Organization'
226 | import { WizardLayout } from 'components/layouts'
227 | -import { useSetupIntent } from 'data/stripe/setup-intent-mutation'
228 | -import { STRIPE_PUBLIC_KEY } from 'lib/constants'
229 | import { useIsHCaptchaLoaded } from 'stores/hcaptcha-loaded-store'
230 | import type { NextPageWithLayout } from 'types'
231 |
232 | -const stripePromise = loadStripe(STRIPE_PUBLIC_KEY)
233 | -
234 | /**
235 | * No org selected yet, create a new one
236 | */
237 | const Wizard: NextPageWithLayout = () => {
238 | - const { resolvedTheme } = useTheme()
239 | -
240 | - const [intent, setIntent] = useState()
241 | const captchaLoaded = useIsHCaptchaLoaded()
242 |
243 | const [captchaToken, setCaptchaToken] = useState(null)
244 | const [captchaRef, setCaptchaRef] = useState(null)
245 |
246 | - const { mutate: setupIntent } = useSetupIntent({ onSuccess: (res) => setIntent(res) })
247 | -
248 | const captchaRefCallback = useCallback((node: any) => {
249 | setCaptchaRef(node)
250 | }, [])
251 |
252 | - const initSetupIntent = async (hcaptchaToken: string | undefined) => {
253 | - if (!hcaptchaToken) return console.error('Hcaptcha token is required')
254 | -
255 | - // Force a reload of Elements, necessary for Stripe
256 | - setIntent(undefined)
257 | - setupIntent({ hcaptchaToken })
258 | - }
259 | -
260 | - const options = {
261 | - clientSecret: intent ? intent.client_secret : '',
262 | - appearance: { theme: resolvedTheme?.includes('dark') ? 'night' : 'flat', labels: 'floating' },
263 | - } as any
264 | -
265 | const loadPaymentForm = async () => {
266 | if (captchaRef && captchaLoaded) {
267 | let token = captchaToken
268 | @@ -57,22 +32,15 @@ const Wizard: NextPageWithLayout = () => {
269 | return
270 | }
271 |
272 | - await initSetupIntent(token ?? undefined)
273 | resetCaptcha()
274 | }
275 | }
276 |
277 | - useEffect(() => {
278 | - loadPaymentForm()
279 | - }, [captchaRef, captchaLoaded])
280 | -
281 | const resetSetupIntent = () => {
282 | return loadPaymentForm()
283 | }
284 |
285 | - const onLocalCancel = () => {
286 | - setIntent(undefined)
287 | - }
288 | + const onLocalCancel = () => {}
289 |
290 | const resetCaptcha = () => {
291 | setCaptchaToken(null)
292 | @@ -94,11 +62,7 @@ const Wizard: NextPageWithLayout = () => {
293 | }}
294 | />
295 |
296 | - {intent && (
297 | -
298 | - resetSetupIntent()} />
299 | -
300 | - )}
301 | + resetSetupIntent()} />
302 | >
303 | )
304 | }
305 |
--------------------------------------------------------------------------------
/studio/patches/15-fixprojcreation.patch:
--------------------------------------------------------------------------------
1 | diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx
2 | index 583f058..be4ce27 100644
3 | --- a/apps/studio/pages/new/[slug].tsx
4 | +++ b/apps/studio/pages/new/[slug].tsx
5 | @@ -179,6 +179,7 @@ const Wizard: NextPageWithLayout = () => {
6 | toast.error(
7 | `Invalid Postgres version, should start with a number between 12-19, a dot and additional characters, i.e. 15.2 or 15.2.0-3`
8 | )
9 | + return
10 | }
11 |
12 | data['customSupabaseRequest'] = {
13 | @@ -383,11 +384,7 @@ const Wizard: NextPageWithLayout = () => {
14 | label="Postgres Version"
15 | autoComplete="off"
16 | descriptionText={
17 | -
18 | - Specify a custom version of Postgres (Defaults to the latest)
19 | -
20 | - This is only applicable for local/staging projects
21 | -
22 | + Specify a custom version of Postgres (Defaults to the latest)
23 | }
24 | type="text"
25 | placeholder="Postgres Version"
26 |
--------------------------------------------------------------------------------
/studio/patches/16-removepropsprefix.patch:
--------------------------------------------------------------------------------
1 | diff --git a/apps/studio/data/config/project-api-query.ts b/apps/studio/data/config/project-api-query.ts
2 | index ae1e960..87dd75f 100644
3 | --- a/apps/studio/data/config/project-api-query.ts
4 | +++ b/apps/studio/data/config/project-api-query.ts
5 | @@ -66,7 +66,7 @@ export async function getProjectApi({ projectRef }: ProjectApiVariables, signal?
6 | throw new Error('projectRef is required')
7 | }
8 |
9 | - const response = await get(`${API_URL}/props/project/${projectRef}/api`, {
10 | + const response = await get(`${API_URL}/project/${projectRef}/api`, {
11 | signal,
12 | })
13 | if (response.error) {
14 | diff --git a/apps/studio/data/config/project-settings-query.ts b/apps/studio/data/config/project-settings-query.ts
15 | index 7123a01..d326c7d 100644
16 | --- a/apps/studio/data/config/project-settings-query.ts
17 | +++ b/apps/studio/data/config/project-settings-query.ts
18 | @@ -29,7 +29,8 @@ export async function getProjectSettings(
19 | if (!projectRef) throw new Error('projectRef is required')
20 |
21 | // [Joshen] API typing is wrong here
22 | - const { data, error } = await get('/platform/props/project/{ref}/settings', {
23 | + // @ts-ignore
24 | + const { data, error } = await get('/platform/project/{ref}/settings', {
25 | params: { path: { ref: projectRef } },
26 | signal,
27 | })
28 | diff --git a/apps/studio/pages/vercel/integrate.tsx b/apps/studio/pages/vercel/integrate.tsx
29 | index 0fdbd91..da4ffa8 100644
30 | --- a/apps/studio/pages/vercel/integrate.tsx
31 | +++ b/apps/studio/pages/vercel/integrate.tsx
32 | @@ -350,7 +350,7 @@ const ProjectLinks = observer(() => {
33 | continue
34 | }
35 | // If not, pull project detail info
36 | - const projectDetails = await get(`${API_URL}/props/project/${item.supabaseProjectRef}/api`)
37 | + const projectDetails = await get(`${API_URL}/project/${item.supabaseProjectRef}/api`)
38 | if (projectDetails.error) {
39 | console.error('project info error: ', projectDetails.error)
40 | runInAction(() => {
41 |
--------------------------------------------------------------------------------
/supa-manager/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL=postgres://postgres:postgres@localhost:5432/supabase
2 | ALLOW_SIGNUP=true
3 | JWT_SECRET=secret
4 | ENCRYPTION_SECRET=secret
5 |
6 | # Service which provides the latest version of the Supabase services, can be hosted locally
7 | SERVICE_VERSION_URL=https://supamanager.io/updates
8 |
9 | # Postgres settings, all have the defaults shown below
10 | POSTGRES_DISK_SIZE=10
11 | POSTGRES_DEFAULT_VERSION=14.2
12 | POSTGRES_DOCKER_IMAGE=supabase/postgres
13 |
14 | # Location of the Studio frontend
15 | DOMAIN_STUDIO_URL=http://localhost:3000
16 | # Used for the project urls i.e. https://flying-rocket.supamanager.io
17 | DOMAIN_BASE=supamanager.io
18 |
19 | # Used to dynamically configure DNS records
20 | DOMAIN_DNS_HOOK_URL=http://localhost:8081
21 | DOMAIN_DNS_HOOK_KEY=mysecretkey
--------------------------------------------------------------------------------
/supa-manager/Dockerfile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HarryET/supa-manager/b97bf28e97b8cae3268aad5928b78e2e44aee83e/supa-manager/Dockerfile
--------------------------------------------------------------------------------
/supa-manager/api/api.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "github.com/gin-contrib/cors"
8 | "github.com/gin-gonic/gin"
9 | "github.com/golang-jwt/jwt/v5"
10 | "github.com/jackc/pgx/v5/pgxpool"
11 | "github.com/matthewhartstonge/argon2"
12 | "log/slog"
13 | "net/http"
14 | "supamanager.io/supa-manager/conf"
15 | "supamanager.io/supa-manager/database"
16 | "time"
17 | )
18 |
19 | type Api struct {
20 | isHealthy bool
21 | logger *slog.Logger
22 | config *conf.Config
23 | queries *database.Queries
24 | pgPool *pgxpool.Pool
25 | argon argon2.Config
26 | }
27 |
28 | func CreateApi(logger *slog.Logger, config *conf.Config) (*Api, error) {
29 | conn, err := pgxpool.New(context.Background(), config.DatabaseUrl)
30 | if err != nil {
31 | logger.Error(fmt.Sprintf("Unable to connect to database: %v", err))
32 | return nil, err
33 | }
34 |
35 | if err := conf.EnsureMigrationsTableExists(conn); err != nil {
36 | logger.Error(fmt.Sprintf("Failed to ensure migrations table: %v", err))
37 | return nil, err
38 | }
39 |
40 | queries := database.New(conn)
41 |
42 | if success, err := conf.EnsureMigrations(conn, queries); err != nil || !success {
43 | logger.Error(fmt.Sprintf("Failed to run migrations: %v", err))
44 | return nil, err
45 | }
46 |
47 | return &Api{
48 | logger: logger,
49 | config: config,
50 | queries: queries,
51 | pgPool: conn,
52 | argon: argon2.DefaultConfig(),
53 | }, nil
54 | }
55 |
56 | func (a *Api) GetAccountIdFromRequest(c *gin.Context) (string, error) {
57 | authHeader := c.GetHeader("Authorization")
58 | if authHeader == "" {
59 | return "", errors.New("missing Authorization header")
60 | }
61 |
62 | tokenString := authHeader[len("Bearer "):]
63 | token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
64 | return []byte(a.config.JwtSecret), nil
65 | })
66 | if err != nil {
67 | return "", err
68 | }
69 |
70 | claims, ok := token.Claims.(*jwt.RegisteredClaims)
71 | if !ok {
72 | return "", errors.New("invalid token claims")
73 | }
74 |
75 | return claims.Subject, nil
76 | }
77 |
78 | func (a *Api) GetAccountFromRequest(c *gin.Context) (*database.Account, error) {
79 | id, err := a.GetAccountIdFromRequest(c)
80 | if err != nil {
81 | return nil, err
82 | }
83 |
84 | if id == "" {
85 | return nil, errors.New("missing account ID")
86 | }
87 |
88 | account, err := a.queries.GetAccountByGoTrueID(c.Request.Context(), id)
89 | if err != nil {
90 | return nil, err
91 | }
92 |
93 | return &account, nil
94 | }
95 |
96 | func (a *Api) ListenAddress() string {
97 | return ":8080"
98 | }
99 |
100 | func (a *Api) index(c *gin.Context) {
101 | c.JSON(http.StatusOK, gin.H{"status": "OK"})
102 | }
103 |
104 | func (a *Api) status(c *gin.Context) {
105 | c.JSON(http.StatusOK, gin.H{"is_healthy": a.isHealthy})
106 | }
107 |
108 | func (a *Api) telemetry(c *gin.Context) {
109 | c.AbortWithStatus(http.StatusNoContent)
110 | }
111 |
112 | const INDEX = ""
113 |
114 | func (a *Api) Router() *gin.Engine {
115 | r := gin.Default()
116 |
117 | r.Use(cors.New(cors.Config{
118 | AllowOrigins: []string{"*"},
119 | AllowMethods: []string{"GET", "POST", "PUT", "PATCH"},
120 | AllowHeaders: []string{"*"},
121 | AllowCredentials: true,
122 | MaxAge: 12 * time.Hour,
123 | }))
124 |
125 | r.GET("/", a.index)
126 | r.GET("/status", a.status)
127 |
128 | profile := r.Group("/profile")
129 | {
130 | profile.GET(INDEX, a.getProfile)
131 | profile.GET("/permissions", a.getProfilePermissions)
132 | profile.POST("/password-check", a.postPasswordCheck)
133 | }
134 |
135 | organization := r.Group("/organizations")
136 | {
137 | organization.GET(INDEX, a.getOrganizations)
138 |
139 | specificOrganization := organization.Group("/:slug")
140 | {
141 | members := specificOrganization.Group("/members")
142 | {
143 | members.GET("/reached-free-project-limit", a.getOrganizationMembersReachedFreeProjectLimit)
144 | }
145 | }
146 | }
147 |
148 | projects := r.Group("/projects")
149 | {
150 | specificProject := projects.Group("/:ref")
151 | {
152 | specificProject.GET("/status", a.getProjectStatus)
153 | specificProject.GET("/jwt-secret-update-status", a.getProjectJwtSecretUpdateStatus)
154 | specificProject.GET("/api", a.getProjectApi)
155 | }
156 | }
157 |
158 | gotrue := r.Group("/auth")
159 | {
160 | gotrue.POST("/token", a.postGotrueToken)
161 | }
162 |
163 | platform := r.Group("/platform")
164 | {
165 | platform.POST("/signup", a.postPlatformSignup)
166 | platform.GET("/notifications", a.getPlatformNotifications)
167 | platform.GET("/stripe/invoices/overdue", a.getPlatformOverdueInvoices)
168 |
169 | platformProjects := platform.Group("/projects")
170 | {
171 | platformProjects.GET(INDEX, a.getPlatformProjects)
172 | platformProjects.POST(INDEX, a.postPlatformProjects)
173 | specificProject := platformProjects.Group("/:ref")
174 | {
175 | specificProject.GET(INDEX, a.getPlatformProject)
176 | specificProject.GET("/settings", a.getPlatformProjectSettings)
177 | }
178 | }
179 |
180 | platformOrganizations := platform.Group("/organizations")
181 | {
182 | platformOrganizations.POST(INDEX, a.postPlatformOrganizations)
183 | specificOrganization := platformOrganizations.Group("/:slug")
184 | {
185 | specificOrganization.GET("/billing/subscription", a.getPlatformOrganizationSubscription)
186 | }
187 | }
188 |
189 | platform.GET("/integrations/:integration/connections", a.getIntegrationConnections)
190 | }
191 |
192 | configcat := r.Group("/configcat")
193 | {
194 | configcat.GET("/configuration-files/:key/config_v5.json", a.getConfigCatConfiguration)
195 | }
196 |
197 | return r
198 | }
199 |
--------------------------------------------------------------------------------
/supa-manager/api/configCat.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "net/http"
6 | )
7 |
8 | func (a *Api) getConfigCatConfiguration(c *gin.Context) {
9 | c.JSON(http.StatusOK, gin.H{
10 | "p": gin.H{
11 | "u": "https://cdn-global.configcat.com",
12 | "r": 0,
13 | },
14 | "f": gin.H{},
15 | })
16 | }
17 |
--------------------------------------------------------------------------------
/supa-manager/api/getIntegrationConnections.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "net/http"
6 | )
7 |
8 | func (a *Api) getIntegrationConnections(c *gin.Context) {
9 | _, err := a.GetAccountFromRequest(c)
10 | if err != nil {
11 | c.JSON(401, gin.H{"error": "Unauthorized"})
12 | return
13 | }
14 |
15 | c.JSON(http.StatusOK, gin.H{"connections": []interface{}{}})
16 | }
17 |
--------------------------------------------------------------------------------
/supa-manager/api/getOrganizationMembersReachedFreeProjectLimit.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "net/http"
6 | )
7 |
8 | func (a *Api) getOrganizationMembersReachedFreeProjectLimit(c *gin.Context) {
9 | _, err := a.GetAccountFromRequest(c)
10 | if err != nil {
11 | c.JSON(401, gin.H{"error": "Unauthorized"})
12 | return
13 | }
14 |
15 | c.JSON(http.StatusOK, []interface{}{})
16 | }
17 |
--------------------------------------------------------------------------------
/supa-manager/api/getOrganizations.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "strings"
6 | )
7 |
8 | type Organization struct {
9 | Slug string `json:"slug"`
10 | Name string `json:"name"`
11 | StripeCustomerId string `json:"stripe_customer_id"`
12 | SubscriptionId string `json:"subscription_id"`
13 | BillingEmail interface{} `json:"billing_email"`
14 | IsOwner bool `json:"is_owner"`
15 | OptInTags []interface{} `json:"opt_in_tags"`
16 | Id int32 `json:"id"`
17 | RestrictionData interface{} `json:"restriction_data"`
18 | RestrictionStatus interface{} `json:"restriction_status"`
19 | }
20 |
21 | func (a *Api) getOrganizations(c *gin.Context) {
22 | account, err := a.GetAccountFromRequest(c)
23 | if err != nil {
24 | c.JSON(401, gin.H{"error": "Unauthorized"})
25 | return
26 | }
27 |
28 | orgs, err := a.queries.GetOrganizationsForAccountId(c, account.ID)
29 | if err != nil {
30 | c.JSON(500, gin.H{"error": "Internal Server Error"})
31 | return
32 | }
33 |
34 | supaOrgs := []Organization{}
35 | for _, org := range orgs {
36 | supaOrgs = append(supaOrgs, Organization{
37 | Slug: org.Slug,
38 | Name: org.Name,
39 | StripeCustomerId: "",
40 | SubscriptionId: "",
41 | BillingEmail: "billing@supamanager.io",
42 | IsOwner: strings.ToLower(org.MemberRole) == "owner",
43 | OptInTags: []interface{}{},
44 | Id: org.ID,
45 | RestrictionData: nil,
46 | RestrictionStatus: nil,
47 | })
48 | }
49 |
50 | c.JSON(200, supaOrgs)
51 | }
52 |
--------------------------------------------------------------------------------
/supa-manager/api/getPlatformNotifications.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "net/http"
6 | )
7 |
8 | func (a *Api) getPlatformNotifications(c *gin.Context) {
9 | _, err := a.GetAccountFromRequest(c)
10 | if err != nil {
11 | println(err.Error())
12 | c.JSON(401, gin.H{"error": "Unauthorized"})
13 | return
14 | }
15 |
16 | c.JSON(http.StatusOK, []interface{}{})
17 | }
18 |
--------------------------------------------------------------------------------
/supa-manager/api/getPlatformOrganizationSubscription.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import "github.com/gin-gonic/gin"
4 |
5 | type PlatformSubscriptionOrganizationSubscriptionBody struct {
6 | NanoEnabled bool `json:"nano_enabled"`
7 | BillingViaPartner bool `json:"billing_via_partner"`
8 | CurrentPeriodEnd int `json:"current_period_end"`
9 | CurrentPeriodStart int `json:"current_period_start"`
10 | NextInvoiceAt int `json:"next_invoice_at"`
11 | CustomerBalance int `json:"customer_balance"`
12 | Plan struct {
13 | Id string `json:"id"`
14 | Name string `json:"name"`
15 | } `json:"plan"`
16 | UsageBillingEnabled bool `json:"usage_billing_enabled"`
17 | Addons []interface{} `json:"addons"`
18 | ProjectAddons []interface{} `json:"project_addons"`
19 | PaymentMethodType string `json:"payment_method_type"`
20 | }
21 |
22 | func (a *Api) getPlatformOrganizationSubscription(c *gin.Context) {
23 | _, err := a.GetAccountFromRequest(c)
24 | if err != nil {
25 | c.JSON(401, gin.H{"error": "Unauthorized"})
26 | return
27 | }
28 |
29 | c.JSON(200, PlatformSubscriptionOrganizationSubscriptionBody{
30 | NanoEnabled: false,
31 | BillingViaPartner: false,
32 | CurrentPeriodEnd: 2147385600, // Jan 18, 2038
33 | CurrentPeriodStart: 0, // Start at 1st Jan 1970
34 | NextInvoiceAt: 2147385600, // Jan 18, 2038
35 | CustomerBalance: 999999, // Balling 💰
36 | Plan: struct {
37 | Id string `json:"id"`
38 | Name string `json:"name"`
39 | }{
40 | Id: "enterprise",
41 | Name: "Enterprise",
42 | },
43 | UsageBillingEnabled: false,
44 | Addons: []interface{}{},
45 | ProjectAddons: []interface{}{},
46 | PaymentMethodType: "none",
47 | })
48 | }
49 |
--------------------------------------------------------------------------------
/supa-manager/api/getPlatformOverdueInvoices.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "net/http"
6 | )
7 |
8 | func (a *Api) getPlatformOverdueInvoices(c *gin.Context) {
9 | _, err := a.GetAccountFromRequest(c)
10 | if err != nil {
11 | c.JSON(401, gin.H{"error": "Unauthorized"})
12 | return
13 | }
14 |
15 | c.JSON(http.StatusOK, []interface{}{})
16 | }
17 |
--------------------------------------------------------------------------------
/supa-manager/api/getPlatformProject.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "net/http"
6 | )
7 |
8 | func (a *Api) getPlatformProject(c *gin.Context) {
9 | _, err := a.GetAccountFromRequest(c)
10 | if err != nil {
11 | c.JSON(401, gin.H{"error": "Unauthorized"})
12 | return
13 | }
14 |
15 | projectRef := c.Param("ref")
16 | project, err := a.queries.GetProjectByRef(c, projectRef)
17 | if err != nil {
18 | c.JSON(500, gin.H{"error": "Internal Server Error"})
19 | return
20 | }
21 |
22 | c.JSON(http.StatusOK, Project{
23 | Id: project.ID,
24 | Ref: project.ProjectRef,
25 | Name: project.ProjectName,
26 | Status: project.Status,
27 | OrganizationId: project.OrganizationID,
28 | InsertedAt: "",
29 | SubscriptionId: "-",
30 | CloudProvider: "k8s",
31 | Region: "mars-1",
32 | DiskVolumeSizeGb: 0,
33 | Size: "",
34 | DbUserSupabase: "",
35 | DbPassSupabase: "",
36 | DbDnsName: "",
37 | DbHost: "",
38 | DbPort: 0,
39 | DbName: "",
40 | SslEnforced: false,
41 | WalgEnabled: false,
42 | InfraComputeSize: "",
43 | PreviewBranchRefs: []interface{}{},
44 | IsBranchEnabled: false,
45 | IsPhysicalBackupsEnabled: false,
46 | })
47 | }
48 |
--------------------------------------------------------------------------------
/supa-manager/api/getPlatformProjectSettings.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "fmt"
5 | "github.com/gin-gonic/gin"
6 | "net/http"
7 | )
8 |
9 | func (a *Api) getPlatformProjectSettings(c *gin.Context) {
10 | _, err := a.GetAccountFromRequest(c)
11 | if err != nil {
12 | c.JSON(401, gin.H{"error": "Unauthorized"})
13 | return
14 | }
15 |
16 | projectRef := c.Param("ref")
17 | proj, err := a.queries.GetProjectByRef(c, projectRef)
18 | if err != nil {
19 | c.JSON(500, gin.H{"error": "Internal Server Error"})
20 | return
21 | }
22 |
23 | c.JSON(http.StatusOK, gin.H{
24 | "project": Project{
25 | Id: proj.ID,
26 | Ref: proj.ProjectRef,
27 | Name: proj.ProjectName,
28 | Status: proj.Status,
29 | OrganizationId: proj.OrganizationID,
30 | InsertedAt: "",
31 | SubscriptionId: "-",
32 | CloudProvider: "k8s",
33 | Region: "mars-1",
34 | DiskVolumeSizeGb: 0,
35 | Size: "",
36 | DbUserSupabase: "",
37 | DbPassSupabase: "",
38 | DbDnsName: "",
39 | DbHost: "",
40 | DbPort: 0,
41 | DbName: "",
42 | SslEnforced: false,
43 | WalgEnabled: false,
44 | InfraComputeSize: "",
45 | PreviewBranchRefs: []interface{}{},
46 | IsBranchEnabled: false,
47 | IsPhysicalBackupsEnabled: false,
48 | },
49 | "services": []interface{}{
50 | ProjectAutoApiService{
51 | Id: 0,
52 | Name: "Default API",
53 | AppConfig: struct {
54 | Endpoint string `json:"endpoint"`
55 | DbSchema string `json:"db_schema"`
56 | }{
57 | Endpoint: fmt.Sprintf("%s.supamanager.io", proj.ProjectRef),
58 | DbSchema: "public",
59 | },
60 | App: struct {
61 | Id int `json:"id"`
62 | Name string `json:"name"`
63 | }{
64 | Id: 1,
65 | Name: "Auto API",
66 | },
67 | ServiceApiKeys: []struct {
68 | Tags string `json:"tags"`
69 | Name string `json:"name"`
70 | }{
71 | {
72 | Tags: "anon",
73 | Name: "anon key",
74 | },
75 | {
76 | Tags: "service_role",
77 | Name: "service_role key",
78 | },
79 | },
80 | Protocol: "https",
81 | Endpoint: fmt.Sprintf("%s.supamanager.io", proj.ProjectRef),
82 | RestUrl: fmt.Sprintf("https://%s.supamanager.io/rest/v1/", proj.ProjectRef),
83 | Project: struct {
84 | Ref string `json:"ref"`
85 | }{
86 | Ref: proj.ProjectRef,
87 | },
88 | DefaultApiKey: "a.b.c",
89 | ServiceApiKey: "a.b.c",
90 | },
91 | },
92 | })
93 | }
94 |
--------------------------------------------------------------------------------
/supa-manager/api/getPlatformProjects.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "net/http"
6 | )
7 |
8 | func (a *Api) getPlatformProjects(c *gin.Context) {
9 | account, err := a.GetAccountFromRequest(c)
10 | if err != nil {
11 | c.JSON(401, gin.H{"error": "Unauthorized"})
12 | return
13 | }
14 |
15 | projects, err := a.queries.GetProjectsForAccountId(c, account.ID)
16 | if err != nil {
17 | c.JSON(500, gin.H{"error": "Internal Server Error"})
18 | return
19 | }
20 |
21 | supaProjects := []Project{}
22 | for _, project := range projects {
23 | supaProjects = append(supaProjects, Project{
24 | Id: project.ID,
25 | Ref: project.ProjectRef,
26 | Name: project.ProjectName,
27 | Status: "INACTIVE",
28 | OrganizationId: project.OrganizationID,
29 | InsertedAt: "",
30 | SubscriptionId: "-",
31 | CloudProvider: "k8s",
32 | Region: "mars-1",
33 | DiskVolumeSizeGb: 0,
34 | Size: "",
35 | DbUserSupabase: "",
36 | DbPassSupabase: "",
37 | DbDnsName: "",
38 | DbHost: "",
39 | DbPort: 0,
40 | DbName: "",
41 | SslEnforced: false,
42 | WalgEnabled: false,
43 | InfraComputeSize: "",
44 | PreviewBranchRefs: []interface{}{},
45 | IsBranchEnabled: false,
46 | IsPhysicalBackupsEnabled: false,
47 | })
48 | }
49 |
50 | c.JSON(http.StatusOK, supaProjects)
51 | }
52 |
--------------------------------------------------------------------------------
/supa-manager/api/getProfile.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "supamanager.io/supa-manager/utils"
6 | )
7 |
8 | type ProfileReturn struct {
9 | Id int32 `json:"id"`
10 | Auth0Id *string `json:"auth0_id"`
11 | PrimaryEmail string `json:"primary_email"`
12 | Username *string `json:"username"`
13 | FirstName *string `json:"first_name"`
14 | LastName *string `json:"last_name"`
15 | Mobile *string `json:"mobile"`
16 | IsAlphaUser bool `json:"is_alpha_user"`
17 | GotrueId string `json:"gotrue_id"`
18 | FreeProjectLimit int `json:"free_project_limit"`
19 | DisabledFeatures []interface{} `json:"disabled_features"`
20 | }
21 |
22 | func (a *Api) getProfile(c *gin.Context) {
23 | account, err := a.GetAccountFromRequest(c)
24 | if err != nil {
25 | c.JSON(401, gin.H{"error": "Unauthorized"})
26 | return
27 | }
28 |
29 | c.JSON(200, ProfileReturn{
30 | Id: account.ID,
31 | Auth0Id: nil,
32 | PrimaryEmail: account.Email,
33 | FirstName: utils.PgTextToPointer(account.FirstName),
34 | LastName: utils.PgTextToPointer(account.LastName),
35 | Mobile: nil,
36 | IsAlphaUser: true,
37 | GotrueId: account.GotrueID,
38 | FreeProjectLimit: 9999,
39 | DisabledFeatures: []interface{}{},
40 | })
41 | }
42 |
--------------------------------------------------------------------------------
/supa-manager/api/getProfilePermissions.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "net/http"
6 | "supamanager.io/supa-manager/permisions"
7 | )
8 |
9 | func (a *Api) getProfilePermissions(c *gin.Context) {
10 | acc, err := a.GetAccountFromRequest(c)
11 | if err != nil {
12 | c.JSON(401, gin.H{"error": "Unauthorized"})
13 | return
14 | }
15 |
16 | orgIds, err := a.queries.GetOrganizationIdsForAccountId(c, acc.ID)
17 | if err != nil {
18 | c.JSON(500, gin.H{"error": "Internal Server Error"})
19 | return
20 | }
21 |
22 | c.JSON(http.StatusOK, permisions.ConstructPermissions(orgIds))
23 | }
24 |
--------------------------------------------------------------------------------
/supa-manager/api/getProjectApi.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "fmt"
5 | "github.com/gin-gonic/gin"
6 | "net/http"
7 | )
8 |
9 | type ProjectAutoApiService struct {
10 | Id int `json:"id"`
11 | Name string `json:"name"`
12 | AppConfig struct {
13 | Endpoint string `json:"endpoint"`
14 | DbSchema string `json:"db_schema"`
15 | } `json:"app_config"`
16 | App struct {
17 | Id int `json:"id"`
18 | Name string `json:"name"`
19 | } `json:"app"`
20 | ServiceApiKeys []struct {
21 | Tags string `json:"tags"`
22 | Name string `json:"name"`
23 | } `json:"service_api_keys"`
24 | Protocol string `json:"protocol"`
25 | Endpoint string `json:"endpoint"`
26 | RestUrl string `json:"restUrl"`
27 | Project struct {
28 | Ref string `json:"ref"`
29 | } `json:"project"`
30 | DefaultApiKey string `json:"defaultApiKey"`
31 | ServiceApiKey string `json:"serviceApiKey"`
32 | }
33 |
34 | func (a *Api) getProjectApi(c *gin.Context) {
35 | _, err := a.GetAccountFromRequest(c)
36 | if err != nil {
37 | c.JSON(401, gin.H{"error": "Unauthorized"})
38 | return
39 | }
40 |
41 | projectRef := c.Param("ref")
42 | proj, err := a.queries.GetProjectByRef(c, projectRef)
43 | if err != nil {
44 | c.JSON(500, gin.H{"error": "Internal Server Error"})
45 | return
46 | }
47 |
48 | c.JSON(http.StatusOK, gin.H{
49 | "autoApiService": ProjectAutoApiService{
50 | Id: 0,
51 | Name: "Default API",
52 | AppConfig: struct {
53 | Endpoint string `json:"endpoint"`
54 | DbSchema string `json:"db_schema"`
55 | }{
56 | Endpoint: fmt.Sprintf("%s.supamanager.io", proj.ProjectRef),
57 | DbSchema: "public",
58 | },
59 | App: struct {
60 | Id int `json:"id"`
61 | Name string `json:"name"`
62 | }{
63 | Id: 1,
64 | Name: "Auto API",
65 | },
66 | ServiceApiKeys: []struct {
67 | Tags string `json:"tags"`
68 | Name string `json:"name"`
69 | }{
70 | {
71 | Tags: "anon",
72 | Name: "anon key",
73 | },
74 | {
75 | Tags: "service_role",
76 | Name: "service_role key",
77 | },
78 | },
79 | Protocol: "https",
80 | Endpoint: fmt.Sprintf("%s.supamanager.io", proj.ProjectRef),
81 | RestUrl: fmt.Sprintf("https://%s.supamanager.io/rest/v1/", proj.ProjectRef),
82 | Project: struct {
83 | Ref string `json:"ref"`
84 | }{
85 | Ref: proj.ProjectRef,
86 | },
87 | DefaultApiKey: "a.b.c",
88 | ServiceApiKey: "a.b.c",
89 | },
90 | })
91 | }
92 |
--------------------------------------------------------------------------------
/supa-manager/api/getProjectJwtSecretUpdateStatus.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "net/http"
6 | )
7 |
8 | func (a *Api) getProjectJwtSecretUpdateStatus(c *gin.Context) {
9 | _, err := a.GetAccountFromRequest(c)
10 | if err != nil {
11 | c.JSON(401, gin.H{"error": "Unauthorized"})
12 | return
13 | }
14 |
15 | projectRef := c.Param("ref")
16 | _, err = a.queries.GetProjectByRef(c, projectRef)
17 | if err != nil {
18 | c.JSON(500, gin.H{"error": "Internal Server Error"})
19 | return
20 | }
21 |
22 | c.JSON(http.StatusOK, gin.H{
23 | "jwtSecretUpdateStatus": nil,
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/supa-manager/api/getProjectStatus.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "net/http"
6 | )
7 |
8 | func (a *Api) getProjectStatus(c *gin.Context) {
9 | _, err := a.GetAccountFromRequest(c)
10 | if err != nil {
11 | c.JSON(401, gin.H{"error": "Unauthorized"})
12 | return
13 | }
14 |
15 | projectRef := c.Param("ref")
16 | project, err := a.queries.GetProjectByRef(c, projectRef)
17 | if err != nil {
18 | c.JSON(500, gin.H{"error": "Internal Server Error"})
19 | return
20 | }
21 |
22 | c.JSON(http.StatusOK, gin.H{"status": project.Status})
23 | }
24 |
--------------------------------------------------------------------------------
/supa-manager/api/postGotrueToken.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/golang-jwt/jwt/v5"
6 | "github.com/matthewhartstonge/argon2"
7 | "time"
8 | )
9 |
10 | type GotrueToken struct {
11 | Email string `json:"email"`
12 | Password string `json:"password"`
13 | GotrueMetaSecurity struct {
14 | CaptchaToken string `json:"captcha_token"`
15 | } `json:"gotrue_meta_security"`
16 | }
17 |
18 | func (a *Api) postGotrueToken(c *gin.Context) {
19 | var body GotrueToken
20 | if err := c.ShouldBindJSON(&body); err != nil {
21 | c.JSON(400, gin.H{"error": "Invalid request"})
22 | return
23 | }
24 |
25 | account, err := a.queries.GetAccountByEmail(c.Request.Context(), body.Email)
26 | if err != nil {
27 | c.JSON(404, gin.H{"error": "Account not found"})
28 | return
29 | }
30 |
31 | if verified, err := argon2.VerifyEncoded([]byte(body.Password), []byte(account.PasswordHash)); err != nil || !verified {
32 | c.JSON(401, gin.H{"error": "Invalid password"})
33 | return
34 | }
35 |
36 | claims := jwt.RegisteredClaims{
37 | Issuer: "supamanager.io",
38 | Subject: account.GotrueID,
39 | Audience: []string{"supamanager.io"},
40 | ExpiresAt: &jwt.NumericDate{Time: time.Now().AddDate(0, 0, 1)},
41 | NotBefore: &jwt.NumericDate{Time: time.Now()},
42 | IssuedAt: &jwt.NumericDate{Time: time.Now()},
43 | }
44 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
45 | // TODO: use a real secret
46 | signedJwt, err := token.SignedString([]byte(a.config.JwtSecret))
47 | if err != nil {
48 | c.JSON(500, gin.H{"error": "Internal server error"})
49 | return
50 | }
51 |
52 | c.JSON(200, gin.H{
53 | "access_token": signedJwt,
54 | "token_type": "Bearer",
55 | "expires_in": 86400,
56 | // TODO: support refresh tokens
57 | "refresh_token": signedJwt,
58 | // TODO: look at GoTrue code for more fields
59 | "user": gin.H{
60 | "id": account.ID,
61 | "email": account.Email,
62 | "app_metadata": gin.H{
63 | "provider": "email",
64 | },
65 | },
66 | })
67 | }
68 |
--------------------------------------------------------------------------------
/supa-manager/api/postPasswordCheck.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/trustelem/zxcvbn"
6 | "net/http"
7 | )
8 |
9 | type PasswordCheckBody struct {
10 | Password string `json:"password"`
11 | }
12 |
13 | func (a *Api) postPasswordCheck(c *gin.Context) {
14 | var body PasswordCheckBody
15 | if err := c.ShouldBindJSON(&body); err != nil {
16 | c.JSON(400, gin.H{"error": "Bad Request"})
17 | return
18 | }
19 |
20 | result := zxcvbn.PasswordStrength(body.Password, nil)
21 |
22 | c.JSON(http.StatusOK, gin.H{
23 | "result": gin.H{
24 | "score": result.Score,
25 | },
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/supa-manager/api/postPlatformOrganizations.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "net/http"
6 | "supamanager.io/supa-manager/database"
7 | )
8 |
9 | type CreateOrgParams struct {
10 | Name string `json:"name"`
11 | Kind string `json:"kind"`
12 | Size string `json:"size"`
13 | Tier string `json:"tier"`
14 | }
15 |
16 | // TODO: use tx
17 | func (a *Api) postPlatformOrganizations(c *gin.Context) {
18 | account, err := a.GetAccountFromRequest(c)
19 | if err != nil {
20 | c.JSON(401, gin.H{"error": "Unauthorized"})
21 | return
22 | }
23 |
24 | var params CreateOrgParams
25 | if err := c.ShouldBindJSON(¶ms); err != nil {
26 | c.JSON(400, gin.H{"error": "Bad Request"})
27 | return
28 | }
29 |
30 | org, err := a.queries.CreateOrganization(c.Request.Context(), params.Name)
31 | if err != nil {
32 | println(err.Error())
33 | c.JSON(500, gin.H{"error": "Internal Server Error"})
34 | return
35 | }
36 |
37 | _, err = a.queries.CreateOrganizationMembership(c.Request.Context(), database.CreateOrganizationMembershipParams{
38 | OrganizationID: org.ID,
39 | AccountID: account.ID,
40 | Role: "OWNER",
41 | })
42 |
43 | c.AbortWithStatus(http.StatusCreated)
44 | }
45 |
--------------------------------------------------------------------------------
/supa-manager/api/postPlatformProjects.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "fmt"
5 | "github.com/gin-gonic/gin"
6 | "github.com/google/uuid"
7 | "github.com/tjarratt/babble"
8 | "net/http"
9 | "strings"
10 | "supamanager.io/supa-manager/database"
11 | )
12 |
13 | type ProjectCreationBody struct {
14 | CloudProvider string `json:"cloud_provider"`
15 | OrgId int32 `json:"org_id"`
16 | Name string `json:"name"`
17 | DbPass string `json:"db_pass"`
18 | DbRegion string `json:"db_region"`
19 | CustomSupabaseInternalRequests struct {
20 | Ami struct {
21 | SearchTags struct {
22 | TagPostgresVersion string `json:"tag:postgresVersion"`
23 | } `json:"search_tags"`
24 | } `json:"ami"`
25 | } `json:"custom_supabase_internal_requests"`
26 | DesiredInstanceSize string `json:"desired_instance_size"`
27 | }
28 |
29 | type ProjectCreationResponse struct {
30 | Id int32 `json:"id"`
31 | Ref string `json:"ref"`
32 | Name string `json:"name"`
33 | Status string `json:"status"`
34 | OrganizationId int32 `json:"organization_id"`
35 | CloudProvider string `json:"cloud_provider"`
36 | Region string `json:"region"`
37 | InsertedAt string `json:"inserted_at"`
38 | Endpoint string `json:"endpoint"`
39 | AnonKey string `json:"anon_key"`
40 | ServiceKey string `json:"service_key"`
41 | IsBranchEnabled bool `json:"is_branch_enabled"`
42 | PreviewBranchRefs []string `json:"preview_branch_refs"`
43 | IsPhysicalBackupsEnabled bool `json:"is_physical_backups_enabled"`
44 | IsReadReplicasEnabled bool `json:"is_read_replicas_enabled"`
45 | DiskVolumeSizeGb int32 `json:"disk_volume_size_gb"`
46 | SubscriptionId string `json:"subscription_id"`
47 | }
48 |
49 | func (a *Api) postPlatformProjects(c *gin.Context) {
50 | _, err := a.GetAccountFromRequest(c)
51 | if err != nil {
52 | c.JSON(401, gin.H{"error": "Unauthorized"})
53 | return
54 | }
55 |
56 | var createProject ProjectCreationBody
57 | if err := c.BindJSON(&createProject); err != nil {
58 | c.JSON(400, gin.H{"error": "Bad Request"})
59 | return
60 | }
61 |
62 | proj, err := a.queries.CreateProject(c.Request.Context(), database.CreateProjectParams{
63 | ProjectRef: strings.ToLower(babble.NewBabbler().Babble()),
64 | ProjectName: createProject.Name,
65 | OrganizationID: createProject.OrgId,
66 | JwtSecret: uuid.New().String(),
67 | CloudProvider: strings.ToUpper(createProject.CloudProvider),
68 | Region: strings.ToUpper(createProject.DbRegion),
69 | })
70 |
71 | if err != nil {
72 | c.JSON(500, gin.H{"error": "Internal Server Error"})
73 | return
74 | }
75 |
76 | c.JSON(http.StatusCreated, ProjectCreationResponse{
77 | Id: proj.ID,
78 | Ref: proj.ProjectRef,
79 | Name: proj.ProjectName,
80 | Status: proj.Status,
81 | OrganizationId: proj.OrganizationID,
82 | CloudProvider: proj.CloudProvider,
83 | Region: proj.Region,
84 | InsertedAt: proj.CreatedAt.Time.Format("2006-01-02T15:04:05.999Z"),
85 | Endpoint: fmt.Sprintf("https://%s.%s", proj.ProjectRef, a.config.Domain.Base),
86 | AnonKey: "a.b.c",
87 | ServiceKey: "a.b.c",
88 | IsBranchEnabled: false,
89 | PreviewBranchRefs: []string{},
90 | IsPhysicalBackupsEnabled: false,
91 | IsReadReplicasEnabled: false,
92 | DiskVolumeSizeGb: 0,
93 | SubscriptionId: "wedontbill",
94 | })
95 | }
96 |
--------------------------------------------------------------------------------
/supa-manager/api/postPlatformSignup.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/jackc/pgx/v5"
6 | "supamanager.io/supa-manager/database"
7 | )
8 |
9 | type PlatformSignupBody struct {
10 | Email string `json:"email"`
11 | Password string `json:"password"`
12 | HcaptchaToken string `json:"hcaptchaToken"`
13 | RedirectTo string `json:"redirectTo"`
14 | }
15 |
16 | func (a *Api) postPlatformSignup(c *gin.Context) {
17 | var body PlatformSignupBody
18 | if err := c.ShouldBindJSON(&body); err != nil {
19 | c.JSON(400, gin.H{"error": "Invalid request"})
20 | return
21 | }
22 |
23 | if !a.config.AllowSignup {
24 | c.JSON(403, gin.H{"error": "Signup is disabled"})
25 | return
26 | }
27 |
28 | _, err := a.queries.GetAccountByEmail(c.Request.Context(), body.Email)
29 | if err == nil {
30 | c.JSON(409, gin.H{"error": "Email already in use"})
31 | return
32 | }
33 |
34 | if err != pgx.ErrNoRows {
35 | c.JSON(500, gin.H{"error": "Internal server error"})
36 | return
37 | }
38 |
39 | hash, err := a.argon.HashEncoded([]byte(body.Password))
40 | if err != nil {
41 | c.JSON(500, gin.H{"error": "Internal server error"})
42 | return
43 | }
44 |
45 | _, err = a.queries.CreateAccount(c.Request.Context(), database.CreateAccountParams{
46 | Email: body.Email,
47 | PasswordHash: string(hash),
48 | Username: "idekman",
49 | })
50 |
51 | if err != nil {
52 | c.JSON(500, gin.H{"error": "Internal server error"})
53 | return
54 | }
55 |
56 | c.JSON(200, gin.H{"status": "CREATED"})
57 | }
58 |
--------------------------------------------------------------------------------
/supa-manager/api/projects.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | type Status = string
4 |
5 | const (
6 | StatusActiveHealthy Status = "ACTIVE_HEALTHY"
7 | StatusActiveUnhealthy Status = "ACTIVE_UNHEALTHY"
8 | StatusInitFailed Status = "INIT_FAILED"
9 | StatusUnknown Status = "UNKNOWN"
10 | StatusComingUp Status = "COMING_UP"
11 | StatusGoingDown Status = "GOING_DOWN"
12 | StatusInactive Status = "INACTIVE"
13 | StatusPausing Status = "PAUSING"
14 | StatusRemoved Status = "REMOVED"
15 | StatusRestoring Status = "RESTORING"
16 | StatusUpgrading Status = "UPGRADING"
17 | StatusCreatingProject Status = "CREATING_PROJECT"
18 | StatusRunningMigrations Status = "RUNNING_MIGRATIONS"
19 | StatusMigrationsFailed Status = "MIGRATIONS_FAILED"
20 | StatusMigrationsPassed Status = "MIGRATIONS_PASSED"
21 | StatusFunctionsDeployed Status = "FUNCTIONS_DEPLOYED"
22 | StatusFunctionsFailed Status = "FUNCTIONS_FAILED"
23 | )
24 |
25 | type Project struct {
26 | Id int32 `json:"id"`
27 | Ref string `json:"ref"`
28 | Name string `json:"name"`
29 | Status string `json:"status"`
30 | OrganizationId int32 `json:"organization_id"`
31 | InsertedAt string `json:"inserted_at"`
32 | SubscriptionId string `json:"subscription_id"`
33 | CloudProvider string `json:"cloud_provider"`
34 | Region string `json:"region"`
35 | DiskVolumeSizeGb int32 `json:"disk_volume_size_gb"`
36 | Size string `json:"size"`
37 | DbUserSupabase string `json:"db_user_supabase"`
38 | DbPassSupabase string `json:"db_pass_supabase"`
39 | DbDnsName string `json:"db_dns_name"`
40 | DbHost string `json:"db_host"`
41 | DbPort int32 `json:"db_port"`
42 | DbName string `json:"db_name"`
43 | SslEnforced bool `json:"ssl_enforced"`
44 | WalgEnabled bool `json:"walg_enabled"`
45 | InfraComputeSize string `json:"infra_compute_size"`
46 | PreviewBranchRefs []interface{} `json:"preview_branch_refs"`
47 | IsBranchEnabled bool `json:"is_branch_enabled"`
48 | IsPhysicalBackupsEnabled bool `json:"is_physical_backups_enabled"`
49 | }
50 |
--------------------------------------------------------------------------------
/supa-manager/conf/config.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "github.com/joho/godotenv"
5 | "github.com/kelseyhightower/envconfig"
6 | "os"
7 | )
8 |
9 | type PostgresSettings struct {
10 | DiskSize int `json:"disk_size" split_words:"true" default:"10"`
11 | DefaultVersion string `json:"default_version" split_words:"true" default:"14.2"`
12 | DockerImage string `json:"docker_image" split_words:"true" default:"supabase/postgres"`
13 | }
14 |
15 | type DomainSettings struct {
16 | StudioUrl string `json:"studio_url" split_words:"true" required:"true"`
17 | Base string `json:"base_url" required:"true"`
18 | DnsHookUrl *string `json:"dns_hook_url" split_words:"true"`
19 | DnsHookKey *string `json:"dns_hook_key" split_words:"true"`
20 | }
21 |
22 | type Config struct {
23 | DatabaseUrl string `json:"database_url" split_words:"true" required:"true"`
24 | Port int `json:"port" default:"8080"`
25 | EncryptionSecret string `json:"encryption_secret" split_words:"true" required:"true"`
26 | JwtSecret string `json:"jwt_secret" split_words:"true" required:"true"`
27 | AllowSignup bool `json:"allow_signup" split_words:"true" default:"false"`
28 | ServiceVersionUrl string `json:"service_version_url" split_words:"true" required:"true" default:"https://supamanager.io/updates"`
29 | Domain DomainSettings `json:"domain" required:"true"`
30 | Postgres PostgresSettings `json:"postgres" required:"true"`
31 | }
32 |
33 | func LoadConfig(filename string) (*Config, error) {
34 | if _, err := os.Stat("./.env"); !os.IsNotExist(err) {
35 | if err := loadEnvironment(filename); err != nil {
36 | return nil, err
37 | }
38 | }
39 | config := new(Config)
40 | if err := envconfig.Process("", config); err != nil {
41 | return nil, err
42 | }
43 | return config, nil
44 | }
45 |
46 | func loadEnvironment(filename string) error {
47 | var err error
48 | if filename != "" {
49 | err = godotenv.Load(filename)
50 | } else {
51 | err = godotenv.Load()
52 | // handle if .env file does not exist, this is OK
53 | if os.IsNotExist(err) {
54 | return nil
55 | }
56 | }
57 | return err
58 | }
59 |
--------------------------------------------------------------------------------
/supa-manager/conf/migrations.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/jackc/pgx/v5"
7 | "github.com/jackc/pgx/v5/pgtype"
8 | "github.com/jackc/pgx/v5/pgxpool"
9 | "os"
10 | "strings"
11 | "supamanager.io/supa-manager/database"
12 | )
13 |
14 | func EnsureMigrationsTableExists(conn *pgxpool.Pool) error {
15 | // Check if the table exists
16 | query := `
17 | SELECT EXISTS (
18 | SELECT FROM information_schema.tables
19 | WHERE table_name = 'migrations'
20 | )
21 | `
22 | var exists bool
23 | err := conn.QueryRow(context.Background(), query).Scan(&exists)
24 | if err != nil {
25 | return fmt.Errorf("error checking if migrations table exists: %w", err)
26 | }
27 |
28 | // Create the table if it does not exist
29 | if !exists {
30 | createTableQuery := `
31 | CREATE TABLE IF NOT EXISTS public.migrations
32 | (
33 | id text not null primary key,
34 | note text,
35 | applied_at timestamptz not null default now()
36 | )
37 | `
38 | _, err = conn.Exec(context.Background(), createTableQuery)
39 | if err != nil {
40 | return fmt.Errorf("error creating migrations table: %w", err)
41 | }
42 | }
43 | return nil
44 | }
45 |
46 | func EnsureMigrations(pool *pgxpool.Pool, conn *database.Queries) (bool, error) {
47 | migrations, err := conn.GetMigrations(context.Background())
48 | if err != nil {
49 | return false, err
50 | }
51 |
52 | files, err := os.ReadDir("migrations")
53 | if err != nil {
54 | return false, err
55 | }
56 |
57 | for _, file := range files {
58 | if file.IsDir() {
59 | continue
60 | }
61 | migrationId := strings.Split(file.Name(), "_")[0]
62 | migration, err := os.ReadFile(fmt.Sprintf("migrations/%s", file.Name()))
63 | if err != nil {
64 | return false, err
65 | }
66 |
67 | // check if the migration has been applied
68 | var applied bool
69 | for _, m := range migrations {
70 | if m.ID == migrationId {
71 | applied = true
72 | break
73 | }
74 | }
75 |
76 | if !applied {
77 | tx, err := pool.BeginTx(context.Background(), pgx.TxOptions{})
78 | if err != nil {
79 | return false, err
80 | }
81 | _, err = tx.Exec(context.Background(), string(migration))
82 | if err != nil {
83 | return false, err
84 | }
85 | defer tx.Rollback(context.Background())
86 |
87 | err = conn.WithTx(tx).PutMigration(context.Background(), database.PutMigrationParams{
88 | ID: migrationId,
89 | Note: pgtype.Text{String: "applied automatically on start-up", Valid: true},
90 | })
91 | if err != nil {
92 | return false, err
93 | }
94 | if err = tx.Commit(context.Background()); err != nil {
95 | return false, err
96 | }
97 | }
98 | }
99 |
100 | return true, nil
101 | }
102 |
--------------------------------------------------------------------------------
/supa-manager/database/accounts.sql.go:
--------------------------------------------------------------------------------
1 | // Code generated by sqlc. DO NOT EDIT.
2 | // versions:
3 | // sqlc v1.26.0
4 | // source: accounts.sql
5 |
6 | package database
7 |
8 | import (
9 | "context"
10 |
11 | "github.com/jackc/pgx/v5/pgtype"
12 | )
13 |
14 | const createAccount = `-- name: CreateAccount :one
15 | INSERT INTO public.accounts (email, password_hash, username)
16 | VALUES ($1, $2, $3)
17 | RETURNING id, gotrue_id, email, password_hash, username, first_name, last_name, created_at, updated_at
18 | `
19 |
20 | type CreateAccountParams struct {
21 | Email string
22 | PasswordHash string
23 | Username string
24 | }
25 |
26 | func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) {
27 | row := q.db.QueryRow(ctx, createAccount, arg.Email, arg.PasswordHash, arg.Username)
28 | var i Account
29 | err := row.Scan(
30 | &i.ID,
31 | &i.GotrueID,
32 | &i.Email,
33 | &i.PasswordHash,
34 | &i.Username,
35 | &i.FirstName,
36 | &i.LastName,
37 | &i.CreatedAt,
38 | &i.UpdatedAt,
39 | )
40 | return i, err
41 | }
42 |
43 | const getAccountByEmail = `-- name: GetAccountByEmail :one
44 | SELECT id, gotrue_id, email, password_hash, username, first_name, last_name, created_at, updated_at FROM public.accounts WHERE email = $1
45 | `
46 |
47 | func (q *Queries) GetAccountByEmail(ctx context.Context, email string) (Account, error) {
48 | row := q.db.QueryRow(ctx, getAccountByEmail, email)
49 | var i Account
50 | err := row.Scan(
51 | &i.ID,
52 | &i.GotrueID,
53 | &i.Email,
54 | &i.PasswordHash,
55 | &i.Username,
56 | &i.FirstName,
57 | &i.LastName,
58 | &i.CreatedAt,
59 | &i.UpdatedAt,
60 | )
61 | return i, err
62 | }
63 |
64 | const getAccountByGoTrueID = `-- name: GetAccountByGoTrueID :one
65 | SELECT id, gotrue_id, email, password_hash, username, first_name, last_name, created_at, updated_at FROM public.accounts WHERE gotrue_id = $1
66 | `
67 |
68 | func (q *Queries) GetAccountByGoTrueID(ctx context.Context, gotrueID string) (Account, error) {
69 | row := q.db.QueryRow(ctx, getAccountByGoTrueID, gotrueID)
70 | var i Account
71 | err := row.Scan(
72 | &i.ID,
73 | &i.GotrueID,
74 | &i.Email,
75 | &i.PasswordHash,
76 | &i.Username,
77 | &i.FirstName,
78 | &i.LastName,
79 | &i.CreatedAt,
80 | &i.UpdatedAt,
81 | )
82 | return i, err
83 | }
84 |
85 | const getAccountByID = `-- name: GetAccountByID :one
86 | SELECT id, gotrue_id, email, password_hash, username, first_name, last_name, created_at, updated_at FROM public.accounts WHERE id = $1
87 | `
88 |
89 | func (q *Queries) GetAccountByID(ctx context.Context, id int32) (Account, error) {
90 | row := q.db.QueryRow(ctx, getAccountByID, id)
91 | var i Account
92 | err := row.Scan(
93 | &i.ID,
94 | &i.GotrueID,
95 | &i.Email,
96 | &i.PasswordHash,
97 | &i.Username,
98 | &i.FirstName,
99 | &i.LastName,
100 | &i.CreatedAt,
101 | &i.UpdatedAt,
102 | )
103 | return i, err
104 | }
105 |
106 | const setAccountName = `-- name: SetAccountName :exec
107 | UPDATE public.accounts
108 | SET first_name = $2,
109 | last_name = $3
110 | WHERE id = $1
111 | `
112 |
113 | type SetAccountNameParams struct {
114 | ID int32
115 | FirstName pgtype.Text
116 | LastName pgtype.Text
117 | }
118 |
119 | func (q *Queries) SetAccountName(ctx context.Context, arg SetAccountNameParams) error {
120 | _, err := q.db.Exec(ctx, setAccountName, arg.ID, arg.FirstName, arg.LastName)
121 | return err
122 | }
123 |
--------------------------------------------------------------------------------
/supa-manager/database/db.go:
--------------------------------------------------------------------------------
1 | // Code generated by sqlc. DO NOT EDIT.
2 | // versions:
3 | // sqlc v1.26.0
4 |
5 | package database
6 |
7 | import (
8 | "context"
9 |
10 | "github.com/jackc/pgx/v5"
11 | "github.com/jackc/pgx/v5/pgconn"
12 | )
13 |
14 | type DBTX interface {
15 | Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
16 | Query(context.Context, string, ...interface{}) (pgx.Rows, error)
17 | QueryRow(context.Context, string, ...interface{}) pgx.Row
18 | }
19 |
20 | func New(db DBTX) *Queries {
21 | return &Queries{db: db}
22 | }
23 |
24 | type Queries struct {
25 | db DBTX
26 | }
27 |
28 | func (q *Queries) WithTx(tx pgx.Tx) *Queries {
29 | return &Queries{
30 | db: tx,
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/supa-manager/database/migrations.sql.go:
--------------------------------------------------------------------------------
1 | // Code generated by sqlc. DO NOT EDIT.
2 | // versions:
3 | // sqlc v1.26.0
4 | // source: migrations.sql
5 |
6 | package database
7 |
8 | import (
9 | "context"
10 |
11 | "github.com/jackc/pgx/v5/pgtype"
12 | )
13 |
14 | const getMigration = `-- name: GetMigration :one
15 | SELECT id, note, applied_at FROM public.migrations WHERE id = $1
16 | `
17 |
18 | func (q *Queries) GetMigration(ctx context.Context, id string) (Migration, error) {
19 | row := q.db.QueryRow(ctx, getMigration, id)
20 | var i Migration
21 | err := row.Scan(&i.ID, &i.Note, &i.AppliedAt)
22 | return i, err
23 | }
24 |
25 | const getMigrations = `-- name: GetMigrations :many
26 | SELECT id, note, applied_at FROM public.migrations
27 | `
28 |
29 | func (q *Queries) GetMigrations(ctx context.Context) ([]Migration, error) {
30 | rows, err := q.db.Query(ctx, getMigrations)
31 | if err != nil {
32 | return nil, err
33 | }
34 | defer rows.Close()
35 | var items []Migration
36 | for rows.Next() {
37 | var i Migration
38 | if err := rows.Scan(&i.ID, &i.Note, &i.AppliedAt); err != nil {
39 | return nil, err
40 | }
41 | items = append(items, i)
42 | }
43 | if err := rows.Err(); err != nil {
44 | return nil, err
45 | }
46 | return items, nil
47 | }
48 |
49 | const putMigration = `-- name: PutMigration :exec
50 | INSERT INTO public.migrations (id, note) VALUES ($1, $2)
51 | `
52 |
53 | type PutMigrationParams struct {
54 | ID string
55 | Note pgtype.Text
56 | }
57 |
58 | func (q *Queries) PutMigration(ctx context.Context, arg PutMigrationParams) error {
59 | _, err := q.db.Exec(ctx, putMigration, arg.ID, arg.Note)
60 | return err
61 | }
62 |
--------------------------------------------------------------------------------
/supa-manager/database/models.go:
--------------------------------------------------------------------------------
1 | // Code generated by sqlc. DO NOT EDIT.
2 | // versions:
3 | // sqlc v1.26.0
4 |
5 | package database
6 |
7 | import (
8 | "github.com/jackc/pgx/v5/pgtype"
9 | )
10 |
11 | type Account struct {
12 | ID int32
13 | GotrueID string
14 | Email string
15 | PasswordHash string
16 | Username string
17 | FirstName pgtype.Text
18 | LastName pgtype.Text
19 | CreatedAt pgtype.Timestamptz
20 | UpdatedAt pgtype.Timestamptz
21 | }
22 |
23 | type Migration struct {
24 | ID string
25 | Note pgtype.Text
26 | AppliedAt pgtype.Timestamptz
27 | }
28 |
29 | type Organization struct {
30 | ID int32
31 | Slug string
32 | Name string
33 | CreatedAt pgtype.Timestamptz
34 | UpdatedAt pgtype.Timestamptz
35 | }
36 |
37 | type OrganizationMembership struct {
38 | OrganizationID int32
39 | AccountID int32
40 | Role string
41 | CreatedAt pgtype.Timestamptz
42 | UpdatedAt pgtype.Timestamptz
43 | }
44 |
45 | type Project struct {
46 | ID int32
47 | ProjectRef string
48 | ProjectName string
49 | OrganizationID int32
50 | Status string
51 | CloudProvider string
52 | Region string
53 | JwtSecret string
54 | CreatedAt pgtype.Timestamptz
55 | UpdatedAt pgtype.Timestamptz
56 | }
57 |
--------------------------------------------------------------------------------
/supa-manager/database/organization_membership.sql.go:
--------------------------------------------------------------------------------
1 | // Code generated by sqlc. DO NOT EDIT.
2 | // versions:
3 | // sqlc v1.26.0
4 | // source: organization_membership.sql
5 |
6 | package database
7 |
8 | import (
9 | "context"
10 | )
11 |
12 | const createOrganizationMembership = `-- name: CreateOrganizationMembership :one
13 | INSERT INTO organization_membership (organization_id, account_id, role)
14 | VALUES ($1, $2, $3)
15 | RETURNING organization_id, account_id, role, created_at, updated_at
16 | `
17 |
18 | type CreateOrganizationMembershipParams struct {
19 | OrganizationID int32
20 | AccountID int32
21 | Role string
22 | }
23 |
24 | func (q *Queries) CreateOrganizationMembership(ctx context.Context, arg CreateOrganizationMembershipParams) (OrganizationMembership, error) {
25 | row := q.db.QueryRow(ctx, createOrganizationMembership, arg.OrganizationID, arg.AccountID, arg.Role)
26 | var i OrganizationMembership
27 | err := row.Scan(
28 | &i.OrganizationID,
29 | &i.AccountID,
30 | &i.Role,
31 | &i.CreatedAt,
32 | &i.UpdatedAt,
33 | )
34 | return i, err
35 | }
36 |
--------------------------------------------------------------------------------
/supa-manager/database/organizations.sql.go:
--------------------------------------------------------------------------------
1 | // Code generated by sqlc. DO NOT EDIT.
2 | // versions:
3 | // sqlc v1.26.0
4 | // source: organizations.sql
5 |
6 | package database
7 |
8 | import (
9 | "context"
10 |
11 | "github.com/jackc/pgx/v5/pgtype"
12 | )
13 |
14 | const createOrganization = `-- name: CreateOrganization :one
15 | INSERT INTO public.organizations (name, created_at, updated_at)
16 | VALUES ($1, now(), now())
17 | RETURNING id, slug, name, created_at, updated_at
18 | `
19 |
20 | func (q *Queries) CreateOrganization(ctx context.Context, name string) (Organization, error) {
21 | row := q.db.QueryRow(ctx, createOrganization, name)
22 | var i Organization
23 | err := row.Scan(
24 | &i.ID,
25 | &i.Slug,
26 | &i.Name,
27 | &i.CreatedAt,
28 | &i.UpdatedAt,
29 | )
30 | return i, err
31 | }
32 |
33 | const getOrganizationById = `-- name: GetOrganizationById :one
34 | SELECT id, slug, name, created_at, updated_at FROM public.organizations WHERE slug = $1
35 | `
36 |
37 | func (q *Queries) GetOrganizationById(ctx context.Context, id string) (Organization, error) {
38 | row := q.db.QueryRow(ctx, getOrganizationById, id)
39 | var i Organization
40 | err := row.Scan(
41 | &i.ID,
42 | &i.Slug,
43 | &i.Name,
44 | &i.CreatedAt,
45 | &i.UpdatedAt,
46 | )
47 | return i, err
48 | }
49 |
50 | const getOrganizationIdsForAccountId = `-- name: GetOrganizationIdsForAccountId :many
51 | SELECT o.id
52 | FROM organization_membership om
53 | JOIN organizations o on o.id = om.organization_id
54 | WHERE account_id = $1
55 | `
56 |
57 | func (q *Queries) GetOrganizationIdsForAccountId(ctx context.Context, accountID int32) ([]int32, error) {
58 | rows, err := q.db.Query(ctx, getOrganizationIdsForAccountId, accountID)
59 | if err != nil {
60 | return nil, err
61 | }
62 | defer rows.Close()
63 | var items []int32
64 | for rows.Next() {
65 | var id int32
66 | if err := rows.Scan(&id); err != nil {
67 | return nil, err
68 | }
69 | items = append(items, id)
70 | }
71 | if err := rows.Err(); err != nil {
72 | return nil, err
73 | }
74 | return items, nil
75 | }
76 |
77 | const getOrganizationsForAccountId = `-- name: GetOrganizationsForAccountId :many
78 | SELECT o.id, o.slug, o.name, o.created_at, o.updated_at, om.role as member_role
79 | FROM organization_membership om
80 | JOIN organizations o on o.id = om.organization_id
81 | WHERE account_id = $1
82 | `
83 |
84 | type GetOrganizationsForAccountIdRow struct {
85 | ID int32
86 | Slug string
87 | Name string
88 | CreatedAt pgtype.Timestamptz
89 | UpdatedAt pgtype.Timestamptz
90 | MemberRole string
91 | }
92 |
93 | func (q *Queries) GetOrganizationsForAccountId(ctx context.Context, accountID int32) ([]GetOrganizationsForAccountIdRow, error) {
94 | rows, err := q.db.Query(ctx, getOrganizationsForAccountId, accountID)
95 | if err != nil {
96 | return nil, err
97 | }
98 | defer rows.Close()
99 | var items []GetOrganizationsForAccountIdRow
100 | for rows.Next() {
101 | var i GetOrganizationsForAccountIdRow
102 | if err := rows.Scan(
103 | &i.ID,
104 | &i.Slug,
105 | &i.Name,
106 | &i.CreatedAt,
107 | &i.UpdatedAt,
108 | &i.MemberRole,
109 | ); err != nil {
110 | return nil, err
111 | }
112 | items = append(items, i)
113 | }
114 | if err := rows.Err(); err != nil {
115 | return nil, err
116 | }
117 | return items, nil
118 | }
119 |
--------------------------------------------------------------------------------
/supa-manager/database/projects.sql.go:
--------------------------------------------------------------------------------
1 | // Code generated by sqlc. DO NOT EDIT.
2 | // versions:
3 | // sqlc v1.26.0
4 | // source: projects.sql
5 |
6 | package database
7 |
8 | import (
9 | "context"
10 | )
11 |
12 | const createProject = `-- name: CreateProject :one
13 | INSERT INTO project (project_ref, project_name, organization_id, status, jwt_secret, cloud_provider, region)
14 | VALUES ($1, $2, $3, 'UNKNOWN', $4, $5, $6)
15 | RETURNING id, project_ref, project_name, organization_id, status, cloud_provider, region, jwt_secret, created_at, updated_at
16 | `
17 |
18 | type CreateProjectParams struct {
19 | ProjectRef string
20 | ProjectName string
21 | OrganizationID int32
22 | JwtSecret string
23 | CloudProvider string
24 | Region string
25 | }
26 |
27 | func (q *Queries) CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error) {
28 | row := q.db.QueryRow(ctx, createProject,
29 | arg.ProjectRef,
30 | arg.ProjectName,
31 | arg.OrganizationID,
32 | arg.JwtSecret,
33 | arg.CloudProvider,
34 | arg.Region,
35 | )
36 | var i Project
37 | err := row.Scan(
38 | &i.ID,
39 | &i.ProjectRef,
40 | &i.ProjectName,
41 | &i.OrganizationID,
42 | &i.Status,
43 | &i.CloudProvider,
44 | &i.Region,
45 | &i.JwtSecret,
46 | &i.CreatedAt,
47 | &i.UpdatedAt,
48 | )
49 | return i, err
50 | }
51 |
52 | const getProjectByRef = `-- name: GetProjectByRef :one
53 | SELECT id, project_ref, project_name, organization_id, status, cloud_provider, region, jwt_secret, created_at, updated_at
54 | FROM project
55 | WHERE project_ref = $1
56 | `
57 |
58 | func (q *Queries) GetProjectByRef(ctx context.Context, projectRef string) (Project, error) {
59 | row := q.db.QueryRow(ctx, getProjectByRef, projectRef)
60 | var i Project
61 | err := row.Scan(
62 | &i.ID,
63 | &i.ProjectRef,
64 | &i.ProjectName,
65 | &i.OrganizationID,
66 | &i.Status,
67 | &i.CloudProvider,
68 | &i.Region,
69 | &i.JwtSecret,
70 | &i.CreatedAt,
71 | &i.UpdatedAt,
72 | )
73 | return i, err
74 | }
75 |
76 | const getProjectsForAccountId = `-- name: GetProjectsForAccountId :many
77 | SELECT p.id, p.project_ref, p.project_name, p.organization_id, p.status, p.cloud_provider, p.region, p.jwt_secret, p.created_at, p.updated_at
78 | FROM organization_membership om
79 | JOIN project p on om.organization_id = p.organization_id
80 | WHERE account_id = $1
81 | `
82 |
83 | func (q *Queries) GetProjectsForAccountId(ctx context.Context, accountID int32) ([]Project, error) {
84 | rows, err := q.db.Query(ctx, getProjectsForAccountId, accountID)
85 | if err != nil {
86 | return nil, err
87 | }
88 | defer rows.Close()
89 | var items []Project
90 | for rows.Next() {
91 | var i Project
92 | if err := rows.Scan(
93 | &i.ID,
94 | &i.ProjectRef,
95 | &i.ProjectName,
96 | &i.OrganizationID,
97 | &i.Status,
98 | &i.CloudProvider,
99 | &i.Region,
100 | &i.JwtSecret,
101 | &i.CreatedAt,
102 | &i.UpdatedAt,
103 | ); err != nil {
104 | return nil, err
105 | }
106 | items = append(items, i)
107 | }
108 | if err := rows.Err(); err != nil {
109 | return nil, err
110 | }
111 | return items, nil
112 | }
113 |
--------------------------------------------------------------------------------
/supa-manager/go.mod:
--------------------------------------------------------------------------------
1 | module supamanager.io/supa-manager
2 |
3 | go 1.21.0
4 |
5 | require (
6 | github.com/gin-contrib/cors v1.7.2
7 | github.com/gin-gonic/gin v1.10.0
8 | github.com/golang-jwt/jwt/v5 v5.2.1
9 | github.com/google/uuid v1.6.0
10 | github.com/jackc/pgx/v5 v5.6.0
11 | github.com/joho/godotenv v1.5.1
12 | github.com/kelseyhightower/envconfig v1.4.0
13 | github.com/matthewhartstonge/argon2 v1.0.0
14 | github.com/tjarratt/babble v0.0.0-20210505082055-cbca2a4833c1
15 | github.com/trustelem/zxcvbn v1.0.1
16 | )
17 |
18 | require (
19 | github.com/bytedance/sonic v1.11.8 // indirect
20 | github.com/bytedance/sonic/loader v0.1.1 // indirect
21 | github.com/cloudwego/base64x v0.1.4 // indirect
22 | github.com/cloudwego/iasm v0.2.0 // indirect
23 | github.com/dlclark/regexp2 v1.11.0 // indirect
24 | github.com/gabriel-vasile/mimetype v1.4.4 // indirect
25 | github.com/gin-contrib/sse v0.1.0 // indirect
26 | github.com/go-playground/locales v0.14.1 // indirect
27 | github.com/go-playground/universal-translator v0.18.1 // indirect
28 | github.com/go-playground/validator/v10 v10.22.0 // indirect
29 | github.com/goccy/go-json v0.10.3 // indirect
30 | github.com/google/go-cmp v0.6.0 // indirect
31 | github.com/jackc/pgpassfile v1.0.0 // indirect
32 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
33 | github.com/jackc/puddle/v2 v2.2.1 // indirect
34 | github.com/json-iterator/go v1.1.12 // indirect
35 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect
36 | github.com/kr/text v0.2.0 // indirect
37 | github.com/leodido/go-urn v1.4.0 // indirect
38 | github.com/mattn/go-isatty v0.0.20 // indirect
39 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
40 | github.com/modern-go/reflect2 v1.0.2 // indirect
41 | github.com/onsi/ginkgo v1.16.5 // indirect
42 | github.com/onsi/gomega v1.33.1 // indirect
43 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect
44 | github.com/rogpeppe/go-internal v1.12.0 // indirect
45 | github.com/test-go/testify v1.1.4 // indirect
46 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
47 | github.com/ugorji/go/codec v1.2.12 // indirect
48 | golang.org/x/arch v0.8.0 // indirect
49 | golang.org/x/crypto v0.24.0 // indirect
50 | golang.org/x/net v0.26.0 // indirect
51 | golang.org/x/sync v0.7.0 // indirect
52 | golang.org/x/sys v0.21.0 // indirect
53 | golang.org/x/text v0.16.0 // indirect
54 | google.golang.org/protobuf v1.34.2 // indirect
55 | gopkg.in/yaml.v3 v3.0.1 // indirect
56 | )
57 |
--------------------------------------------------------------------------------
/supa-manager/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log/slog"
5 | "os"
6 | "supamanager.io/supa-manager/api"
7 | "supamanager.io/supa-manager/conf"
8 | )
9 |
10 | func main() {
11 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
12 | logger.Info("Loading config...")
13 | config, err := conf.LoadConfig(".env")
14 | if err != nil {
15 | logger.Error("Failed to load configuration, ensure the required environment variables are set.")
16 | return
17 | }
18 | apiInstance, err := api.CreateApi(logger, config)
19 | if err != nil {
20 | logger.Error("Failed to start API state. ", err.Error())
21 | return
22 | }
23 |
24 | apiInstance.Router().Run(apiInstance.ListenAddress())
25 | }
26 |
--------------------------------------------------------------------------------
/supa-manager/migrations/00_init.sql:
--------------------------------------------------------------------------------
1 | CREATE SCHEMA IF NOT EXISTS public;
2 |
3 | CREATE TABLE IF NOT EXISTS public.migrations
4 | (
5 | id text not null primary key,
6 | note text,
7 | applied_at timestamptz not null default now()
8 | );
9 |
10 | CREATE TABLE IF NOT EXISTS public.accounts
11 | (
12 | id serial not null,
13 | gotrue_id text not null default gen_random_uuid()::text,
14 |
15 | email text not null,
16 | password_hash text not null,
17 |
18 | username text not null,
19 |
20 | first_name text,
21 | last_name text,
22 |
23 | created_at timestamptz not null default now(),
24 | updated_at timestamptz not null default now(),
25 |
26 | primary key (id)
27 | );
28 |
29 | CREATE TABLE IF NOT EXISTS public.organizations
30 | (
31 | id serial not null,
32 | slug text not null default gen_random_uuid()::text,
33 |
34 | name text not null,
35 |
36 | created_at timestamptz not null default now(),
37 | updated_at timestamptz not null default now(),
38 |
39 | primary key (id)
40 | );
41 |
42 | CREATE TABLE IF NOT EXISTS public.organization_membership
43 | (
44 | organization_id int not null,
45 | account_id int not null,
46 |
47 | role text not null, -- todo does this need a change?
48 |
49 | created_at timestamptz not null default now(),
50 | updated_at timestamptz not null default now(),
51 |
52 | primary key (organization_id, account_id)
53 | );
54 |
55 | ALTER TABLE public.organization_membership
56 | ADD CONSTRAINT fk_membership_org FOREIGN KEY (organization_id) REFERENCES organizations (id);
57 |
58 | ALTER TABLE public.organization_membership
59 | ADD CONSTRAINT fk_membership_account FOREIGN KEY (account_id) REFERENCES accounts (id);
60 |
61 | CREATE TABLE IF NOT EXISTS public.project
62 | (
63 | id serial not null,
64 | project_ref text not null,
65 |
66 | project_name text not null,
67 | organization_id int not null,
68 |
69 | status text not null, -- make this an enum
70 |
71 | cloud_provider text not null default 'k8s',
72 | region text not null default 'mars-1',
73 |
74 | jwt_secret text not null,
75 |
76 | created_at timestamptz not null default now(),
77 | updated_at timestamptz not null default now(),
78 |
79 | primary key (id)
80 | );
81 |
82 | ALTER TABLE public.project
83 | ADD CONSTRAINT fk_project_org FOREIGN KEY (organization_id) REFERENCES organizations (id);
84 |
--------------------------------------------------------------------------------
/supa-manager/permisions/org.go:
--------------------------------------------------------------------------------
1 | package permisions
2 |
3 | import (
4 | "encoding/json"
5 | "strconv"
6 | "strings"
7 | )
8 |
9 | func ConstructPermissions(orgIds []int32) interface{} {
10 | permissionStrings := []string{}
11 | for _, orgId := range orgIds {
12 | permissionStrings = append(permissionStrings, strings.ReplaceAll(strings.ReplaceAll(OrgString, "", strconv.Itoa(int(orgId))), "", "1"))
13 | }
14 | permissionsJson := "[" + strings.Join(permissionStrings, ",") + "]"
15 |
16 | var permissions interface{}
17 | err := json.Unmarshal([]byte(permissionsJson), &permissions)
18 | if err != nil {
19 | return nil
20 | }
21 | return permissions
22 | }
23 |
24 | const OrgString = `{
25 | "organization_id": ,
26 | "resources": ["projects", "integrations.vercel_connections", "integrations.github_connections"],
27 | "actions": ["write:Create"],
28 | "condition": null,
29 | "restrictive": false,
30 | "project_ids": []
31 | }, {
32 | "organization_id": ,
33 | "resources": ["projects"],
34 | "actions": ["write:Update"],
35 | "condition": {
36 | "and": [{
37 | "var": "resource.project_id"
38 | }]
39 | },
40 | "restrictive": false,
41 | "project_ids": []
42 | }, {
43 | "organization_id": ,
44 | "resources": ["preview_branches", "approved_oauth_apps", "third_party_auth"],
45 | "actions": ["write:Create", "write:Update", "write:Delete"],
46 | "condition": null,
47 | "restrictive": false,
48 | "project_ids": []
49 | }, {
50 | "organization_id": ,
51 | "resources": ["projects.pgsodium_root_key_encrypted"],
52 | "actions": ["read:Read", "write:Update"],
53 | "condition": null,
54 | "restrictive": false,
55 | "project_ids": []
56 | }, {
57 | "organization_id": ,
58 | "resources": ["%"],
59 | "actions": ["billing:Write", "infra:Execute"],
60 | "condition": null,
61 | "restrictive": false,
62 | "project_ids": []
63 | }, {
64 | "organization_id": ,
65 | "resources": ["notifications", "integrations.vercel_connections", "integrations.github_connections"],
66 | "actions": ["write:Update", "write:Delete"],
67 | "condition": null,
68 | "restrictive": false,
69 | "project_ids": []
70 | }, {
71 | "organization_id": ,
72 | "resources": ["preview_branches"],
73 | "actions": ["write:Create", "write:Update", "write:Delete"],
74 | "condition": {
75 | "!": {
76 | "var": "resource.is_default"
77 | }
78 | },
79 | "restrictive": false,
80 | "project_ids": []
81 | }, {
82 | "organization_id": ,
83 | "resources": ["back_ups", "events"],
84 | "actions": ["write:Create"],
85 | "condition": null,
86 | "restrictive": false,
87 | "project_ids": []
88 | }, {
89 | "organization_id": ,
90 | "resources": ["%"],
91 | "actions": ["infra:Execute"],
92 | "condition": {
93 | "and": [{
94 | "var": "resource_name"
95 | }, {
96 | "!": {
97 | "in": [{
98 | "var": "resource_name"
99 | }, ["queue_jobs.projects.initialize_or_resume", "queue_jobs.projects.pause"]]
100 | }
101 | }]
102 | },
103 | "restrictive": false,
104 | "project_ids": []
105 | }, {
106 | "organization_id": ,
107 | "resources": ["user_content"],
108 | "actions": ["write:Update", "write:Delete"],
109 | "condition": {
110 | "and": [{
111 | "var": "resource.visibility"
112 | }, {
113 | "var": "resource.owner_id"
114 | }, {
115 | "var": "subject.id"
116 | }, {
117 | "or": [{
118 | "!=": [{
119 | "var": "resource.visibility"
120 | }, "user"]
121 | }, {
122 | "==": [{
123 | "var": "resource.owner_id"
124 | }, {
125 | "var": "subject.id"
126 | }]
127 | }]
128 | }]
129 | },
130 | "restrictive": false,
131 | "project_ids": []
132 | }, {
133 | "organization_id": ,
134 | "resources": ["user_content"],
135 | "actions": ["write:Create"],
136 | "condition": {
137 | "and": [{
138 | "var": "resource.owner_id"
139 | }, {
140 | "var": "subject.id"
141 | }, {
142 | "==": [{
143 | "var": "resource.owner_id"
144 | }, {
145 | "var": "subject.id"
146 | }]
147 | }]
148 | },
149 | "restrictive": false,
150 | "project_ids": []
151 | }, {
152 | "organization_id": ,
153 | "resources": ["field.jwt_secret", "service_api_keys.service_role_key"],
154 | "actions": ["read:Read"],
155 | "condition": null,
156 | "restrictive": false,
157 | "project_ids": []
158 | }, {
159 | "organization_id": ,
160 | "resources": ["%"],
161 | "actions": ["auth:Execute", "functions:Write", "storage:Admin:Write", "tenant:Sql:Admin:Write", "tenant:Sql:CreateTable", "tenant:Sql:Query", "tenant:Sql:Write:%"],
162 | "condition": null,
163 | "restrictive": false,
164 | "project_ids": []
165 | }, {
166 | "organization_id": ,
167 | "resources": ["custom_config_gotrue", "custom_config_postgrest", "owner_reassign", "services"],
168 | "actions": ["write:Create", "write:Update"],
169 | "condition": null,
170 | "restrictive": false,
171 | "project_ids": []
172 | }, {
173 | "organization_id": ,
174 | "resources": ["members", "organizations", "auth.subject_roles", "users", "user_invites", "auth.permissions", "auth.roles"],
175 | "actions": ["read:Read"],
176 | "condition": null,
177 | "restrictive": false,
178 | "project_ids": []
179 | }, {
180 | "organization_id": ,
181 | "resources": ["auth.subject_roles"],
182 | "actions": ["write:Delete"],
183 | "condition": {
184 | "and": [{
185 | "var": "resource.subject_id"
186 | }, {
187 | "var": "subject.gotrue_id"
188 | }, {
189 | "==": [{
190 | "var": "resource.subject_id"
191 | }, {
192 | "var": "subject.gotrue_id"
193 | }]
194 | }]
195 | },
196 | "restrictive": false,
197 | "project_ids": []
198 | }, {
199 | "organization_id": ,
200 | "resources": ["user_invites", "auth.permissions", "auth.roles"],
201 | "actions": ["write:Create", "write:Update", "write:Delete"],
202 | "condition": null,
203 | "restrictive": false,
204 | "project_ids": []
205 | }, {
206 | "organization_id": ,
207 | "resources": ["auth.subject_roles"],
208 | "actions": ["write:Create", "write:Delete"],
209 | "condition": null,
210 | "restrictive": false,
211 | "project_ids": []
212 | }, {
213 | "organization_id": ,
214 | "resources": ["organizations"],
215 | "actions": ["write:Update"],
216 | "condition": null,
217 | "restrictive": false,
218 | "project_ids": []
219 | }, {
220 | "organization_id": ,
221 | "resources": ["back_ups", "custom_config_gotrue", "custom_config_postgrest", "customers", "events", "gotrue_config", "infrastructure", "invoices", "member_active_free_projects", "notifications", "organizations", "owner_reassign", "physical_backups", "postgrest_config", "service_api_keys", "services", "stats_daily_projects", "subscriptions", "subscription_items", "preview_branches", "resource_exhaustion_notifications", "approved_oauth_apps", "third_party_auth", "integrations.vercel_connections", "integrations.github_connections"],
222 | "actions": ["read:Read"],
223 | "condition": null,
224 | "restrictive": false,
225 | "project_ids": []
226 | }, {
227 | "organization_id": ,
228 | "resources": ["projects", "third_party_auth"],
229 | "actions": ["read:Read"],
230 | "condition": {
231 | "and": [{
232 | "var": "resource.project_id"
233 | }]
234 | },
235 | "restrictive": false,
236 | "project_ids": []
237 | }, {
238 | "organization_id": ,
239 | "resources": ["user_content_folders"],
240 | "actions": ["read:Read", "write:Create", "write:Update", "write:Delete"],
241 | "condition": {
242 | "and": [{
243 | "var": "resource.owner_id"
244 | }, {
245 | "var": "subject.id"
246 | }, {
247 | "==": [{
248 | "var": "resource.owner_id"
249 | }, {
250 | "var": "subject.id"
251 | }]
252 | }]
253 | },
254 | "restrictive": false,
255 | "project_ids": []
256 | }, {
257 | "organization_id": ,
258 | "resources": ["user_content"],
259 | "actions": ["write:Update", "write:Delete"],
260 | "condition": {
261 | "and": [{
262 | "var": "resource.owner_id"
263 | }, {
264 | "var": "resource.type"
265 | }, {
266 | "var": "resource.visibility"
267 | }, {
268 | "var": "subject.id"
269 | }, {
270 | "and": [{
271 | "!==": [{
272 | "var": "resource.type"
273 | }, "report"]
274 | }, {
275 | "===": [{
276 | "var": "resource.owner_id"
277 | }, {
278 | "var": "subject.id"
279 | }]
280 | }]
281 | }]
282 | },
283 | "restrictive": false,
284 | "project_ids": []
285 | }, {
286 | "organization_id": ,
287 | "resources": ["user_content"],
288 | "actions": ["write:Create"],
289 | "condition": {
290 | "and": [{
291 | "var": "resource.owner_id"
292 | }, {
293 | "var": "resource.type"
294 | }, {
295 | "var": "subject.id"
296 | }, {
297 | "and": [{
298 | "!==": [{
299 | "var": "resource.type"
300 | }, "report"]
301 | }, {
302 | "===": [{
303 | "var": "resource.owner_id"
304 | }, {
305 | "var": "subject.id"
306 | }]
307 | }]
308 | }]
309 | },
310 | "restrictive": false,
311 | "project_ids": []
312 | }, {
313 | "organization_id": ,
314 | "resources": ["user_content"],
315 | "actions": ["read:Read"],
316 | "condition": {
317 | "and": [{
318 | "var": "resource.visibility"
319 | }, {
320 | "var": "resource.owner_id"
321 | }, {
322 | "var": "subject.id"
323 | }, {
324 | "or": [{
325 | "!=": [{
326 | "var": "resource.visibility"
327 | }, "user"]
328 | }, {
329 | "==": [{
330 | "var": "resource.owner_id"
331 | }, {
332 | "var": "subject.id"
333 | }]
334 | }]
335 | }]
336 | },
337 | "restrictive": false,
338 | "project_ids": []
339 | }, {
340 | "organization_id": ,
341 | "resources": ["%"],
342 | "actions": ["analytics:Read", "billing:Read", "functions:Read", "storage:Admin:Read", "tenant:Sql:Admin:Read", "tenant:Sql:Read:Select"],
343 | "condition": null,
344 | "restrictive": false,
345 | "project_ids": []
346 | }, {
347 | "organization_id": ,
348 | "resources": ["auth.subject_roles"],
349 | "actions": ["write:Create", "write:Delete"],
350 | "condition": {
351 | "and": [{
352 | "var": "resource.role_id"
353 | }, {
354 | "!==": [{
355 | "var": "resource.role_id"
356 | }, ]
357 | }]
358 | },
359 | "restrictive": false,
360 | "project_ids": null
361 | }, {
362 | "organization_id": ,
363 | "resources": ["user_invites"],
364 | "actions": ["write:Create", "write:Update", "write:Delete"],
365 | "condition": {
366 | "and": [{
367 | "var": "resource.role_id"
368 | }, {
369 | "!==": [{
370 | "var": "resource.role_id"
371 | }, ]
372 | }]
373 | },
374 | "restrictive": false,
375 | "project_ids": null
376 | }`
377 |
--------------------------------------------------------------------------------
/supa-manager/queries/accounts.sql:
--------------------------------------------------------------------------------
1 | -- name: CreateAccount :one
2 | INSERT INTO public.accounts (email, password_hash, username)
3 | VALUES ($1, $2, $3)
4 | RETURNING *;
5 |
6 | -- name: SetAccountName :exec
7 | UPDATE public.accounts
8 | SET first_name = $2,
9 | last_name = $3
10 | WHERE id = $1;
11 |
12 | -- name: GetAccountByEmail :one
13 | SELECT * FROM public.accounts WHERE email = $1;
14 |
15 | -- name: GetAccountByID :one
16 | SELECT * FROM public.accounts WHERE id = $1;
17 |
18 | -- name: GetAccountByGoTrueID :one
19 | SELECT * FROM public.accounts WHERE gotrue_id = $1;
--------------------------------------------------------------------------------
/supa-manager/queries/migrations.sql:
--------------------------------------------------------------------------------
1 | -- name: GetMigration :one
2 | SELECT * FROM public.migrations WHERE id = $1;
3 |
4 | -- name: GetMigrations :many
5 | SELECT * FROM public.migrations;
6 |
7 | -- name: PutMigration :exec
8 | INSERT INTO public.migrations (id, note) VALUES ($1, $2);
--------------------------------------------------------------------------------
/supa-manager/queries/organization_membership.sql:
--------------------------------------------------------------------------------
1 | -- name: CreateOrganizationMembership :one
2 | INSERT INTO organization_membership (organization_id, account_id, role)
3 | VALUES ($1, $2, $3)
4 | RETURNING *;
--------------------------------------------------------------------------------
/supa-manager/queries/organizations.sql:
--------------------------------------------------------------------------------
1 | -- name: CreateOrganization :one
2 | INSERT INTO public.organizations (name, created_at, updated_at)
3 | VALUES ($1, now(), now())
4 | RETURNING *;
5 |
6 | -- name: GetOrganizationById :one
7 | SELECT * FROM public.organizations WHERE slug = sqlc.arg('id');
8 |
9 | -- name: GetOrganizationsForAccountId :many
10 | SELECT o.*, om.role as member_role
11 | FROM organization_membership om
12 | JOIN organizations o on o.id = om.organization_id
13 | WHERE account_id = $1;
14 |
15 | -- name: GetOrganizationIdsForAccountId :many
16 | SELECT o.id
17 | FROM organization_membership om
18 | JOIN organizations o on o.id = om.organization_id
19 | WHERE account_id = $1;
--------------------------------------------------------------------------------
/supa-manager/queries/projects.sql:
--------------------------------------------------------------------------------
1 | -- name: GetProjectsForAccountId :many
2 | SELECT p.*
3 | FROM organization_membership om
4 | JOIN project p on om.organization_id = p.organization_id
5 | WHERE account_id = $1;
6 |
7 | -- name: CreateProject :one
8 | INSERT INTO project (project_ref, project_name, organization_id, status, jwt_secret, cloud_provider, region)
9 | VALUES ($1, $2, $3, 'UNKNOWN', $4, $5, $6)
10 | RETURNING *;
11 |
12 | -- name: GetProjectByRef :one
13 | SELECT *
14 | FROM project
15 | WHERE project_ref = $1;
--------------------------------------------------------------------------------
/supa-manager/sqlc.yaml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | sql:
3 | - engine: "postgresql"
4 | queries: "./queries"
5 | schema: "./migrations"
6 | gen:
7 | go:
8 | package: "database"
9 | out: "database"
10 | sql_package: "pgx/v5"
--------------------------------------------------------------------------------
/supa-manager/utils/sqlTypes.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/jackc/pgx/v5/pgtype"
5 | )
6 |
7 | func PgTextToPointer(ns pgtype.Text) *string {
8 | if ns.Valid {
9 | return &ns.String
10 | }
11 |
12 | return nil
13 | }
14 |
--------------------------------------------------------------------------------
/version-service/.env.example:
--------------------------------------------------------------------------------
1 | PUSHING_ACCOUNTS=harryet,kiwicopple
2 | LISTEN_ADDRESS=0.0.0.0:8081
3 | DATABASE_URL=postgres://postgres:password@localhost:5432/version_service
--------------------------------------------------------------------------------
/version-service/README.md:
--------------------------------------------------------------------------------
1 | # Version Management Service
2 | This service provides the latest version of the individual containers/services used by supa-manager.
3 |
4 | We have a hosted version available at https://supamanager.io/versions, this is updated as regularly as possible.
5 |
6 | If you don't want any external dependencies you are welcome to host it yourself and either mirror or manually update.
7 |
8 | ## Configuration
9 | - `DATABASE_URL` - PostgresSQL Connection String
10 | - `PUSHING_ACCOUNTS` - Comma separated list of github accounts that are allowed to push new versions (needs a SSH key)
11 | - `LISTEN_ADDRESS` - Address to listen on (default: `0.0.0.0:8081`)
12 |
13 | ## Usage
14 | ### Push new service version
15 | 1. Find the new version's image and tag i.e. `supabase/gotrue` & `0.1.0`
16 | 2. Sign the string `supabase/gotrue:0.1.0` with an allowed private key
17 | 3. Send a POST request to `/:service/update-version` with the following body:
18 | ```json
19 | {
20 | "service": "supabase/gotrue",
21 | "version": "0.1.0"
22 | }
23 | ```
24 | and the header `signature` with the signed string
25 |
26 | ## Getting the latest versions for all services
27 | Send a GET request to `/` and you will receive a JSON object with all the services and their latest versions.
28 |
29 | ## Getting all versions for a specific service
30 | Send a GET request to `/:service` and you will receive a JSON object with all the versions for that service.
31 |
--------------------------------------------------------------------------------
/version-service/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/joho/godotenv"
5 | "github.com/kelseyhightower/envconfig"
6 | "os"
7 | )
8 |
9 | type Config struct {
10 | PushingAccounts []string `json:"pushing_accounts" split_words:"true" required:"true"`
11 | ListenAddress string `json:"listen_address" split_words:"true" default:"0.0.0.0:8080"`
12 | DatabaseUrl string `json:"database_url" split_words:"true" required:"true"`
13 | }
14 |
15 | func LoadConfig(filename string) (*Config, error) {
16 | if _, err := os.Stat("./.env"); !os.IsNotExist(err) {
17 | if err := loadEnvironment(filename); err != nil {
18 | return nil, err
19 | }
20 | }
21 | config := new(Config)
22 | if err := envconfig.Process("", config); err != nil {
23 | return nil, err
24 | }
25 | return config, nil
26 | }
27 |
28 | func loadEnvironment(filename string) error {
29 | var err error
30 | if filename != "" {
31 | err = godotenv.Load(filename)
32 | } else {
33 | err = godotenv.Load()
34 | // handle if .env file does not exist, this is OK
35 | if os.IsNotExist(err) {
36 | return nil
37 | }
38 | }
39 | return err
40 | }
41 |
--------------------------------------------------------------------------------
/version-service/db.go:
--------------------------------------------------------------------------------
1 | // Code generated by sqlc. DO NOT EDIT.
2 | // versions:
3 | // sqlc v1.26.0
4 |
5 | package main
6 |
7 | import (
8 | "context"
9 |
10 | "github.com/jackc/pgx/v5"
11 | "github.com/jackc/pgx/v5/pgconn"
12 | )
13 |
14 | type DBTX interface {
15 | Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
16 | Query(context.Context, string, ...interface{}) (pgx.Rows, error)
17 | QueryRow(context.Context, string, ...interface{}) pgx.Row
18 | }
19 |
20 | func New(db DBTX) *Queries {
21 | return &Queries{db: db}
22 | }
23 |
24 | type Queries struct {
25 | db DBTX
26 | }
27 |
28 | func (q *Queries) WithTx(tx pgx.Tx) *Queries {
29 | return &Queries{
30 | db: tx,
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/version-service/go.mod:
--------------------------------------------------------------------------------
1 | module supamanager.io/version-service
2 |
3 | go 1.21.0
4 |
5 | require (
6 | github.com/gin-gonic/gin v1.10.0
7 | github.com/jackc/pgx/v5 v5.6.0
8 | github.com/joho/godotenv v1.5.1
9 | github.com/kelseyhightower/envconfig v1.4.0
10 | golang.org/x/crypto v0.23.0
11 | )
12 |
13 | require (
14 | github.com/bytedance/sonic v1.11.6 // indirect
15 | github.com/bytedance/sonic/loader v0.1.1 // indirect
16 | github.com/cloudwego/base64x v0.1.4 // indirect
17 | github.com/cloudwego/iasm v0.2.0 // indirect
18 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect
19 | github.com/gin-contrib/sse v0.1.0 // indirect
20 | github.com/go-playground/locales v0.14.1 // indirect
21 | github.com/go-playground/universal-translator v0.18.1 // indirect
22 | github.com/go-playground/validator/v10 v10.20.0 // indirect
23 | github.com/goccy/go-json v0.10.2 // indirect
24 | github.com/jackc/pgpassfile v1.0.0 // indirect
25 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
26 | github.com/jackc/puddle/v2 v2.2.1 // indirect
27 | github.com/json-iterator/go v1.1.12 // indirect
28 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect
29 | github.com/kr/text v0.2.0 // indirect
30 | github.com/leodido/go-urn v1.4.0 // indirect
31 | github.com/mattn/go-isatty v0.0.20 // indirect
32 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
33 | github.com/modern-go/reflect2 v1.0.2 // indirect
34 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect
35 | github.com/rogpeppe/go-internal v1.12.0 // indirect
36 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
37 | github.com/ugorji/go/codec v1.2.12 // indirect
38 | golang.org/x/arch v0.8.0 // indirect
39 | golang.org/x/net v0.25.0 // indirect
40 | golang.org/x/sync v0.1.0 // indirect
41 | golang.org/x/sys v0.20.0 // indirect
42 | golang.org/x/text v0.15.0 // indirect
43 | google.golang.org/protobuf v1.34.1 // indirect
44 | gopkg.in/yaml.v3 v3.0.1 // indirect
45 | )
46 |
--------------------------------------------------------------------------------
/version-service/go.sum:
--------------------------------------------------------------------------------
1 | github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
2 | github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
3 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
4 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
5 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
6 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
7 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
8 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
9 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
10 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
11 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
15 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
16 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
17 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
18 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
19 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
20 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
21 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
22 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
23 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
24 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
25 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
26 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
27 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
28 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
29 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
30 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
31 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
32 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
33 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
34 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
35 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
36 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
37 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
38 | github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
39 | github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
40 | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
41 | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
42 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
43 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
44 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
45 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
46 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
47 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
48 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
49 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
50 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
51 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
52 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
53 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
54 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
55 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
56 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
57 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
58 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
59 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
60 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
61 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
62 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
63 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
64 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
65 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
66 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
67 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
68 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
69 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
70 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
71 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
72 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
73 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
74 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
75 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
76 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
77 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
78 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
79 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
80 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
81 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
82 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
83 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
84 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
85 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
86 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
87 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
88 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
89 | go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
90 | go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
91 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
92 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
93 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
94 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
95 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
96 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
97 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
98 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
99 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
100 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
101 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
102 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
103 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
104 | golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
105 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
106 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
107 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
108 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
109 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
110 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
111 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
112 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
113 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
114 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
115 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
116 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
117 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
118 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
119 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
120 |
--------------------------------------------------------------------------------
/version-service/keys.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto"
5 | "crypto/ecdsa"
6 | "crypto/rsa"
7 | "crypto/sha256"
8 | "encoding/asn1"
9 | "fmt"
10 | "golang.org/x/crypto/ssh"
11 | "io"
12 | "math/big"
13 | "net/http"
14 | "strings"
15 | )
16 |
17 | func VerifySignature(pubKey crypto.PublicKey, message, signature []byte) error {
18 | hashed := sha256.Sum256(message)
19 |
20 | switch key := pubKey.(type) {
21 | case *rsa.PublicKey:
22 | return rsa.VerifyPKCS1v15(key, crypto.SHA256, hashed[:], signature)
23 | case *ecdsa.PublicKey:
24 | var ecdsaSig struct {
25 | R, S *big.Int
26 | }
27 | if _, err := asn1.Unmarshal(signature, &ecdsaSig); err != nil {
28 | return err
29 | }
30 | if !ecdsa.Verify(key, hashed[:], ecdsaSig.R, ecdsaSig.S) {
31 | return fmt.Errorf("ecdsa: verification failed")
32 | }
33 | return nil
34 | default:
35 | return fmt.Errorf("unsupported key type %T", pubKey)
36 | }
37 | }
38 |
39 | type AccountKey struct {
40 | Key crypto.PublicKey
41 | Username string
42 | }
43 |
44 | // For each of the accounts load the SSH keys from github i.e. https://github.com/.keys
45 | func LoadSSHKeys(config *Config) ([]AccountKey, error) {
46 | var sshKeys []AccountKey
47 | for _, account := range config.PushingAccounts {
48 | keys, err := loadKeys(account)
49 | if err != nil {
50 | return nil, err
51 | }
52 | sshKeys = append(sshKeys, keys...)
53 | }
54 | return sshKeys, nil
55 | }
56 |
57 | func loadKeys(account string) ([]AccountKey, error) {
58 | // make a http request to github.com/account.keys
59 | res, err := http.Get(fmt.Sprintf("https://github.com/%s.keys", account))
60 | if err != nil {
61 | return nil, err
62 | }
63 |
64 | // read the response body
65 | body, err := io.ReadAll(res.Body)
66 | if err != nil {
67 | return nil, err
68 | }
69 |
70 | // close the response body
71 | err = res.Body.Close()
72 | if err != nil {
73 | return nil, err
74 | }
75 |
76 | // split the body by new line
77 | keys := strings.Split(string(body), "\n")
78 |
79 | // create a slice to store the keys
80 | var sshKeys []AccountKey
81 | for _, key := range keys {
82 | // parse the key
83 | parsedKey, err := parseKey(key)
84 | if err != nil {
85 | return nil, err
86 | }
87 | // append the key to the slice
88 | sshKeys = append(sshKeys, AccountKey{
89 | Key: parsedKey,
90 | Username: strings.ToLower(account),
91 | })
92 | }
93 |
94 | // return the keys
95 | return sshKeys, nil
96 | }
97 |
98 | func parseKey(sshPubKey string) (crypto.PublicKey, error) {
99 | pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshPubKey))
100 | if err != nil {
101 | return nil, err
102 | }
103 |
104 | switch key := pubKey.(type) {
105 | case *ssh.Certificate:
106 | return key.Key.(crypto.PublicKey), nil
107 | default:
108 | return key.(crypto.PublicKey), nil
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/version-service/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/gin-gonic/gin"
7 | "github.com/jackc/pgx/v5/pgxpool"
8 | "strings"
9 | )
10 |
11 | type UpdateVersionRequest struct {
12 | Image string `json:"image" required:"true"`
13 | Tag string `json:"tag" required:"true"`
14 | }
15 |
16 | func main() {
17 | config, err := LoadConfig(".env")
18 | if err != nil {
19 | println("Failed to load configuration, ensure the required environment variables are set.")
20 | return
21 | }
22 | sshKeys, err := LoadSSHKeys(config)
23 | if err != nil {
24 | println("Failed to load SSH keys. Please ensure all Github accounts have SSH keys.")
25 | return
26 | }
27 |
28 | conn, err := pgxpool.New(context.Background(), config.DatabaseUrl)
29 | if err != nil {
30 | println(fmt.Sprintf("Unable to connect to database: %v", err))
31 | return
32 | }
33 |
34 | queries := New(conn)
35 |
36 | r := gin.Default()
37 |
38 | r.GET("/", func(c *gin.Context) {
39 | versions, err := queries.GetVersions(c.Request.Context())
40 | if err != nil {
41 | c.JSON(500, gin.H{"error": "Internal Server Error"})
42 | return
43 | }
44 |
45 | c.JSON(200, versions)
46 | })
47 |
48 | r.GET("/:service", func(c *gin.Context) {
49 | service := c.Param("service")
50 | versions, err := queries.GetVersionsForService(c.Request.Context(), strings.ToLower(service))
51 | if err != nil {
52 | c.JSON(500, gin.H{"error": "Internal Server Error"})
53 | return
54 | }
55 |
56 | c.JSON(200, versions)
57 | })
58 |
59 | r.POST("/:service/update-version", func(c *gin.Context) {
60 | signature := c.GetHeader("Signature")
61 | if signature == "" {
62 | c.JSON(400, gin.H{"error": "missing Signature header"})
63 | return
64 | }
65 |
66 | service := c.Param("service")
67 |
68 | var body UpdateVersionRequest
69 | if err := c.ShouldBindJSON(&body); err != nil {
70 | c.JSON(400, gin.H{"error": err.Error()})
71 | return
72 | }
73 |
74 | isValidSig := false
75 | var account string
76 | for _, key := range sshKeys {
77 | if err := VerifySignature(key.Key, []byte(fmt.Sprintf("%s:%s", body.Image, body.Tag)), []byte(signature)); err == nil {
78 | isValidSig = true
79 | account = key.Username
80 | break
81 | }
82 | }
83 |
84 | if !isValidSig {
85 | c.JSON(401, gin.H{"error": "Unauthorized"})
86 | return
87 | }
88 |
89 | svc, err := queries.CreateNewVersion(c.Request.Context(), CreateNewVersionParams{
90 | ServiceID: strings.ToLower(service),
91 | Image: body.Image,
92 | Tag: body.Tag,
93 | CreatedBy: strings.ToLower(account),
94 | })
95 |
96 | if err != nil {
97 | c.JSON(500, gin.H{"error": "Internal Server Error"})
98 | return
99 | }
100 |
101 | c.JSON(200, gin.H{
102 | "id": svc.ID,
103 | })
104 | })
105 |
106 | r.Run(config.ListenAddress)
107 | }
108 |
--------------------------------------------------------------------------------
/version-service/models.go:
--------------------------------------------------------------------------------
1 | // Code generated by sqlc. DO NOT EDIT.
2 | // versions:
3 | // sqlc v1.26.0
4 |
5 | package main
6 |
7 | import (
8 | "github.com/jackc/pgx/v5/pgtype"
9 | )
10 |
11 | type Version struct {
12 | ID pgtype.UUID
13 | ServiceID string
14 | Image string
15 | Tag string
16 | CreatedAt pgtype.Timestamp
17 | CreatedBy string
18 | }
19 |
--------------------------------------------------------------------------------
/version-service/queries.sql:
--------------------------------------------------------------------------------
1 | -- name: CreateNewVersion :one
2 | INSERT INTO versions (service_id, image, tag, created_by)
3 | VALUES ($1, $2, $3, $4)
4 | RETURNING *;
5 |
6 | -- name: GetVersionsForService :many
7 | SELECT *
8 | FROM versions
9 | WHERE service_id = $1
10 | ORDER BY created_at DESC;
11 |
12 | -- name: GetVersions :many
13 | SELECT jsonb_object_agg(service_id, jsonb_build_object(
14 | 'image', image,
15 | 'tag', tag,
16 | 'created_at', created_at,
17 | 'created_by', created_by
18 | )) AS result
19 | FROM (SELECT service_id,
20 | image,
21 | tag,
22 | created_at,
23 | created_by
24 | FROM versions v1
25 | WHERE created_at = (SELECT MAX(created_at)
26 | FROM versions v2
27 | WHERE v2.service_id = v1.service_id)) subquery;
28 |
29 |
30 |
--------------------------------------------------------------------------------
/version-service/queries.sql.go:
--------------------------------------------------------------------------------
1 | // Code generated by sqlc. DO NOT EDIT.
2 | // versions:
3 | // sqlc v1.26.0
4 | // source: queries.sql
5 |
6 | package main
7 |
8 | import (
9 | "context"
10 | )
11 |
12 | const createNewVersion = `-- name: CreateNewVersion :one
13 | INSERT INTO versions (service_id, image, tag, created_by)
14 | VALUES ($1, $2, $3, $4)
15 | RETURNING id, service_id, image, tag, created_at, created_by
16 | `
17 |
18 | type CreateNewVersionParams struct {
19 | ServiceID string
20 | Image string
21 | Tag string
22 | CreatedBy string
23 | }
24 |
25 | func (q *Queries) CreateNewVersion(ctx context.Context, arg CreateNewVersionParams) (Version, error) {
26 | row := q.db.QueryRow(ctx, createNewVersion,
27 | arg.ServiceID,
28 | arg.Image,
29 | arg.Tag,
30 | arg.CreatedBy,
31 | )
32 | var i Version
33 | err := row.Scan(
34 | &i.ID,
35 | &i.ServiceID,
36 | &i.Image,
37 | &i.Tag,
38 | &i.CreatedAt,
39 | &i.CreatedBy,
40 | )
41 | return i, err
42 | }
43 |
44 | const getVersions = `-- name: GetVersions :many
45 | SELECT jsonb_object_agg(service_id, jsonb_build_object(
46 | 'image', image,
47 | 'tag', tag,
48 | 'created_at', created_at,
49 | 'created_by', created_by
50 | )) AS result
51 | FROM (SELECT service_id,
52 | image,
53 | tag,
54 | created_at,
55 | created_by
56 | FROM versions v1
57 | WHERE created_at = (SELECT MAX(created_at)
58 | FROM versions v2
59 | WHERE v2.service_id = v1.service_id)) subquery
60 | `
61 |
62 | func (q *Queries) GetVersions(ctx context.Context) ([][]byte, error) {
63 | rows, err := q.db.Query(ctx, getVersions)
64 | if err != nil {
65 | return nil, err
66 | }
67 | defer rows.Close()
68 | var items [][]byte
69 | for rows.Next() {
70 | var result []byte
71 | if err := rows.Scan(&result); err != nil {
72 | return nil, err
73 | }
74 | items = append(items, result)
75 | }
76 | if err := rows.Err(); err != nil {
77 | return nil, err
78 | }
79 | return items, nil
80 | }
81 |
82 | const getVersionsForService = `-- name: GetVersionsForService :many
83 | SELECT id, service_id, image, tag, created_at, created_by
84 | FROM versions
85 | WHERE service_id = $1
86 | ORDER BY created_at DESC
87 | `
88 |
89 | func (q *Queries) GetVersionsForService(ctx context.Context, serviceID string) ([]Version, error) {
90 | rows, err := q.db.Query(ctx, getVersionsForService, serviceID)
91 | if err != nil {
92 | return nil, err
93 | }
94 | defer rows.Close()
95 | var items []Version
96 | for rows.Next() {
97 | var i Version
98 | if err := rows.Scan(
99 | &i.ID,
100 | &i.ServiceID,
101 | &i.Image,
102 | &i.Tag,
103 | &i.CreatedAt,
104 | &i.CreatedBy,
105 | ); err != nil {
106 | return nil, err
107 | }
108 | items = append(items, i)
109 | }
110 | if err := rows.Err(); err != nil {
111 | return nil, err
112 | }
113 | return items, nil
114 | }
115 |
--------------------------------------------------------------------------------
/version-service/schema.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS versions
2 | (
3 | id uuid not null primary key default gen_random_uuid(),
4 | service_id text not null,
5 | image text not null,
6 | tag text not null,
7 | created_at timestamp not null default now(),
8 | created_by text not null
9 | );
--------------------------------------------------------------------------------
/version-service/sqlc.yaml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | sql:
3 | - engine: "postgresql"
4 | queries: "./queries.sql"
5 | schema: "./schema.sql"
6 | gen:
7 | go:
8 | package: "main"
9 | out: "."
10 | sql_package: "pgx/v5"
--------------------------------------------------------------------------------