Skip to content

Commit 8f67753

Browse files
committed
feat: creating auth access templates
- Creating different templates for 'functions new' command - Handling '--auth' flag to specify which template use
1 parent d03a45b commit 8f67753

7 files changed

Lines changed: 159 additions & 38 deletions

File tree

cmd/functions.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ var (
8585
},
8686
}
8787

88+
authAccessMode = utils.EnumFlag{
89+
Allowed: []string{
90+
string(new_.AuthAccessModeAlways),
91+
string(new_.AuthAccessModeApiKey),
92+
string(new_.AuthAccessModeUser),
93+
},
94+
}
8895
functionsNewCmd = &cobra.Command{
8996
Use: "new <Function name>",
9097
Short: "Create a new Function locally",
@@ -94,7 +101,12 @@ var (
94101
return cmd.Root().PersistentPreRunE(cmd, args)
95102
},
96103
RunE: func(cmd *cobra.Command, args []string) error {
97-
return new_.Run(cmd.Context(), args[0], afero.NewOsFs())
104+
authMode := new_.AuthAccessModeAlways
105+
if len(authAccessMode.Value) > 0 {
106+
authMode = new_.AuthAccessMode(authAccessMode.Value)
107+
}
108+
109+
return new_.Run(cmd.Context(), args[0], authMode, afero.NewOsFs())
98110
},
99111
}
100112

@@ -168,6 +180,7 @@ func init() {
168180
functionsDownloadCmd.MarkFlagsMutuallyExclusive("use-api", "use-docker", "legacy-bundle")
169181
cobra.CheckErr(downloadFlags.MarkHidden("legacy-bundle"))
170182
cobra.CheckErr(downloadFlags.MarkHidden("use-docker"))
183+
functionsNewCmd.Flags().Var(&authAccessMode, "auth", "use a specific auth access (default always)")
171184
functionsCmd.AddCommand(functionsListCmd)
172185
functionsCmd.AddCommand(functionsDeleteCmd)
173186
functionsCmd.AddCommand(functionsDeployCmd)

internal/functions/new/new.go

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,49 @@ import (
1616
"github.com/supabase/cli/internal/utils/flags"
1717
)
1818

19+
type AuthAccessMode string
20+
21+
const (
22+
AuthAccessModeAlways AuthAccessMode = "always"
23+
AuthAccessModeApiKey AuthAccessMode = "apikey"
24+
AuthAccessModeUser AuthAccessMode = "user"
25+
)
26+
1927
var (
20-
//go:embed templates/index.ts
21-
indexEmbed string
28+
//go:embed templates/index_always_access.ts
29+
indexAuthAlwaysEmbed string
30+
//go:embed templates/index_apikey_access.ts
31+
indexAuthApiKeyEmbed string
32+
//go:embed templates/index_user_access.ts
33+
indexAuthUserEmbed string
34+
2235
//go:embed templates/deno.json
2336
denoEmbed string
2437
//go:embed templates/.npmrc
2538
npmrcEmbed string
2639
//go:embed templates/config.toml
2740
configEmbed string
2841

29-
indexTemplate = template.Must(template.New("index").Parse(indexEmbed))
42+
indexAuthTemplates = map[AuthAccessMode]*template.Template{
43+
AuthAccessModeAlways: template.Must(template.New("index").Parse(indexAuthAlwaysEmbed)),
44+
AuthAccessModeApiKey: template.Must(template.New("index").Parse(indexAuthApiKeyEmbed)),
45+
AuthAccessModeUser: template.Must(template.New("index").Parse(indexAuthUserEmbed)),
46+
}
47+
3048
configTemplate = template.Must(template.New("config").Parse(configEmbed))
3149
)
3250

3351
type indexConfig struct {
34-
URL string
35-
Token string
52+
URL string
53+
PublishableKey string
54+
}
55+
56+
type functionConfig struct {
57+
Slug string
58+
VerifyJWT bool
3659
}
3760

38-
func Run(ctx context.Context, slug string, fsys afero.Fs) error {
61+
func Run(ctx context.Context, slug string, authMode AuthAccessMode, fsys afero.Fs) error {
3962
// 1. Sanity checks.
4063
if err := utils.ValidateFunctionSlug(slug); err != nil {
4164
return err
@@ -56,17 +79,18 @@ func Run(ctx context.Context, slug string, fsys afero.Fs) error {
5679
if err := flags.LoadConfig(fsys); err != nil {
5780
fmt.Fprintln(utils.GetDebugLogger(), err)
5881
}
59-
if err := createEntrypointFile(slug, fsys); err != nil {
82+
if err := createEntrypointFile(slug, authMode, fsys); err != nil {
6083
return err
6184
}
62-
if err := appendConfigFile(slug, fsys); err != nil {
85+
verifyJWT := authMode == AuthAccessModeUser
86+
if err := appendConfigFile(slug, verifyJWT, fsys); err != nil {
6387
return err
6488
}
6589
// 3. Create optional files
66-
if err := afero.WriteFile(fsys, filepath.Join(funcDir, "deno.json"), []byte(denoEmbed), 0644); err != nil {
90+
if err := afero.WriteFile(fsys, filepath.Join(funcDir, "deno.json"), []byte(denoEmbed), 0o644); err != nil {
6791
return errors.Errorf("failed to create deno.json config: %w", err)
6892
}
69-
if err := afero.WriteFile(fsys, filepath.Join(funcDir, ".npmrc"), []byte(npmrcEmbed), 0644); err != nil {
93+
if err := afero.WriteFile(fsys, filepath.Join(funcDir, ".npmrc"), []byte(npmrcEmbed), 0o644); err != nil {
7094
return errors.Errorf("failed to create .npmrc config: %w", err)
7195
}
7296
fmt.Println("Created new Function at " + utils.Bold(funcDir))
@@ -79,33 +103,40 @@ func Run(ctx context.Context, slug string, fsys afero.Fs) error {
79103
return nil
80104
}
81105

82-
func createEntrypointFile(slug string, fsys afero.Fs) error {
106+
func createEntrypointFile(slug string, authMode AuthAccessMode, fsys afero.Fs) error {
83107
entrypointPath := filepath.Join(utils.FunctionsDir, slug, "index.ts")
84-
f, err := fsys.OpenFile(entrypointPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
108+
f, err := fsys.OpenFile(entrypointPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
85109
if err != nil {
86110
return errors.Errorf("failed to create entrypoint: %w", err)
87111
}
88112
defer f.Close()
113+
indexTemplate, hasTemplate := indexAuthTemplates[authMode]
114+
if !hasTemplate {
115+
return errors.Errorf("failed to write entrypoint: '%w' is not a valid template", authMode)
116+
}
89117
if err := indexTemplate.Option("missingkey=error").Execute(f, indexConfig{
90-
URL: utils.GetApiUrl("/functions/v1/" + slug),
91-
Token: utils.Config.Auth.AnonKey.Value,
118+
URL: utils.GetApiUrl("/functions/v1/" + slug),
119+
PublishableKey: utils.Config.Auth.PublishableKey.Value,
92120
}); err != nil {
93121
return errors.Errorf("failed to write entrypoint: %w", err)
94122
}
95123
return nil
96124
}
97125

98-
func appendConfigFile(slug string, fsys afero.Fs) error {
126+
func appendConfigFile(slug string, verifyJWT bool, fsys afero.Fs) error {
99127
if _, exists := utils.Config.Functions[slug]; exists {
100128
fmt.Fprintf(os.Stderr, "[functions.%s] is already declared in %s\n", slug, utils.Bold(utils.ConfigPath))
101129
return nil
102130
}
103-
f, err := fsys.OpenFile(utils.ConfigPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
131+
f, err := fsys.OpenFile(utils.ConfigPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
104132
if err != nil {
105133
return errors.Errorf("failed to append config: %w", err)
106134
}
107135
defer f.Close()
108-
if err := configTemplate.Option("missingkey=error").Execute(f, slug); err != nil {
136+
if err := configTemplate.Option("missingkey=error").Execute(f, functionConfig{
137+
Slug: slug,
138+
VerifyJWT: verifyJWT,
139+
}); err != nil {
109140
return errors.Errorf("failed to append template: %w", err)
110141
}
111142
return nil
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11

2-
[functions.{{ . }}]
2+
[functions.{{ .Slug }}]
33
enabled = true
4-
verify_jwt = true
5-
import_map = "./functions/{{ . }}/deno.json"
4+
verify_jwt = {{ .VerifyJWT }}
5+
import_map = "./functions/{{ .Slug }}/deno.json"
66
# Uncomment to specify a custom file path to the entrypoint.
77
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
8-
entrypoint = "./functions/{{ . }}/index.ts"
8+
entrypoint = "./functions/{{ .Slug }}/index.ts"
99
# Specifies static files to be bundled with the function. Supports glob patterns.
1010
# For example, if you want to serve static HTML pages in your function:
11-
# static_files = [ "./functions/{{ . }}/*.html" ]
11+
# static_files = [ "./functions/{{ .Slug }}/*.html" ]
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"imports": {
3-
"@supabase/functions-js": "jsr:@supabase/functions-js@^2"
3+
"@supabase/functions-js": "jsr:@supabase/functions-js@^2",
4+
"@supabase/server": "npm:@supabase/server"
45
}
56
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Follow this setup guide to integrate the Deno language server with your editor:
2+
// https://deno.land/manual/getting_started/setup_your_environment
3+
// This enables autocomplete, go to definition, etc.
4+
5+
// Setup type definitions for built-in Supabase Runtime APIs
6+
import "@supabase/functions-js/edge-runtime.d.ts";
7+
import { withSupabase } from "@supabase/server";
8+
9+
console.log("Hello from Functions!");
10+
11+
// This endpoint uses 'always' access, no credentials required, every request is accepted.
12+
// Use it for health checks, public APIs, or when you handle auth yourself inside the handler.
13+
export default {
14+
fetch: withSupabase({ allow: "always" }, async (req, ctx) => {
15+
const { name } = await req.json();
16+
17+
return Response.json({
18+
message: `Hello ${name}!`,
19+
});
20+
}),
21+
};
22+
23+
/* To invoke locally:
24+
25+
1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
26+
2. Make an HTTP request:
27+
28+
curl -i --location --request POST '{{ .URL }}' \
29+
--header 'Content-Type: application/json' \
30+
--data '{"name":"Functions"}'
31+
32+
*/
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Follow this setup guide to integrate the Deno language server with your editor:
2+
// https://deno.land/manual/getting_started/setup_your_environment
3+
// This enables autocomplete, go to definition, etc.
4+
5+
// Setup type definitions for built-in Supabase Runtime APIs
6+
import "@supabase/functions-js/edge-runtime.d.ts";
7+
import { withSupabase } from "@supabase/server";
8+
9+
console.log("Hello from Functions!");
10+
11+
// This endpoint uses 'public' | 'secret' access, apiKey is required.
12+
// Use public for Client-facing, key-validated endpoints
13+
// Use secret for Server-to-server, internal calls
14+
export default {
15+
fetch: withSupabase({ allow: ["public", "secret"] }, async (req, ctx) => {
16+
// Called by another service with a secret key
17+
// ctx.supabaseAdmin bypasses RLS — use for privileged operations
18+
/*
19+
if (ctx.authType === "secret") {
20+
const { user_id } = await req.json();
21+
const { data } = await ctx.supabaseAdmin.auth.admin.getUserById(user_id);
22+
23+
return Response.json({
24+
email: data?.user?.email,
25+
});
26+
}
27+
*/
28+
29+
const { name } = await req.json();
30+
31+
return Response.json({
32+
message: `Hello ${name}!`,
33+
});
34+
}),
35+
};
36+
37+
/* To invoke locally:
38+
39+
1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
40+
2. Make an HTTP request:
41+
42+
curl -i --location --request POST '{{ .URL }}' \
43+
--header 'apiKey: {{ .PublishableKey }}' \
44+
--data '{"name":"Functions"}'
45+
46+
*/

internal/functions/new/templates/index.ts renamed to internal/functions/new/templates/index_user_access.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,27 @@
44

55
// Setup type definitions for built-in Supabase Runtime APIs
66
import "@supabase/functions-js/edge-runtime.d.ts"
7+
import { withSupabase } from '@supabase/server'
78

89
console.log("Hello from Functions!")
910

10-
Deno.serve(async (req) => {
11-
const { name } = await req.json()
12-
const data = {
13-
message: `Hello ${name}!`,
14-
}
11+
// This endpoint uses 'user' access, credentials is required.
12+
export default {
13+
fetch: withSupabase({ allow: 'user' }, async (_req, ctx) => {
14+
const email = ctx.userClaims?.email;
1515

16-
return new Response(
17-
JSON.stringify(data),
18-
{ headers: { "Content-Type": "application/json" } },
19-
)
20-
})
16+
return Response.json({
17+
message: `Hello ${email}!`,
18+
})
19+
}),
20+
}
2121

2222
/* To invoke locally:
2323
2424
1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
2525
2. Make an HTTP request:
2626
2727
curl -i --location --request POST '{{ .URL }}' \
28-
--header 'Authorization: Bearer {{ .Token }}' \
29-
--header 'Content-Type: application/json' \
30-
--data '{"name":"Functions"}'
31-
28+
--header 'apiKey: {{ .PublishableKey }}'
29+
--header 'Authorization: Bearer <UserToken>'
3230
*/

0 commit comments

Comments
 (0)