mirror of
https://github.com/zoriya/blog.git
synced 2025-12-06 06:26:10 +00:00
Create phantom-token example code
This commit is contained in:
16
content/blogs/cancellation/index.md
Normal file
16
content/blogs/cancellation/index.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: "Cancellation in async"
|
||||
description: ""
|
||||
draft: true
|
||||
tags: []
|
||||
---
|
||||
|
||||
How cancellation is implemented adapted:
|
||||
|
||||
## Via a `request-cancelation` parameter
|
||||
(c#, golang)
|
||||
|
||||
## You don't
|
||||
(js (ok there's `AbortControler` for like 3 functions that use it))
|
||||
|
||||
## Special case: bash
|
||||
24
content/blogs/phantom-token/README.md
Normal file
24
content/blogs/phantom-token/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
Simple phantom token example project
|
||||
|
||||
The `auth` module has 3 endpoints:
|
||||
- `/auth/user` `{ "name": "toto" }` -> create a new user & return a session token
|
||||
- `/auth/jwt` -> Convert the session token in the `authorization` header (after the `Bearer ` prefix) to a jwt
|
||||
- `/.well-known/jwks.json` -> Get a jwks (aka a public key) to verify the jwt's signature
|
||||
|
||||
The `api` module only has one enpoint:
|
||||
- `/api/me` -> Returns the content of the jwt it gets in the `authorization` header.
|
||||
|
||||
Normal flow
|
||||
|
||||
```bash
|
||||
TOKEN=$(curl localhost:7891/auth/user -d '{ "name": "toto" }' | jq .token -r)
|
||||
curl localhost:7891/api/me -H "authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
when calling `/api/me`, we call it with the opaque token retrieved from the `/auth/user` endpoint.
|
||||
the gateway will replace intercept this call and send a request to `/auth/jwt` with the `authorization` header.
|
||||
if `/auth/jwt` returns an error (non 2XX response) -> it simply return this response to the client & abort the request
|
||||
else, it will replace the `authorization` header with the one from the `/auth/jwt` (or it's body) and then make the request to `/api/me`
|
||||
|
||||
|
||||
https://doc.traefik.io/traefik/middlewares/http/forwardauth/ for more info on how this works on traefik
|
||||
34
content/blogs/phantom-token/api/.gitignore
vendored
Normal file
34
content/blogs/phantom-token/api/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
20
content/blogs/phantom-token/api/Dockerfile
Normal file
20
content/blogs/phantom-token/api/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM oven/bun
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json bun.lock .
|
||||
RUN bun install --production
|
||||
|
||||
COPY index.ts tsconfig.json .
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN bun build \
|
||||
--compile \
|
||||
--minify-whitespace \
|
||||
--minify-syntax \
|
||||
--target bun \
|
||||
--outfile server \
|
||||
./index.ts
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["./server"]
|
||||
|
||||
32
content/blogs/phantom-token/api/bun.lock
Normal file
32
content/blogs/phantom-token/api/bun.lock
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "back",
|
||||
"dependencies": {
|
||||
"jose": "^6.0.10",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/bun": ["@types/bun@1.2.6", "", { "dependencies": { "bun-types": "1.2.6" } }, "sha512-fY9CAmTdJH1Llx7rugB0FpgWK2RKuHCs3g2cFDYXUutIy1QGiPQxKkGY8owhfZ4MXWNfxwIbQLChgH5gDsY7vw=="],
|
||||
|
||||
"@types/node": ["@types/node@22.13.13", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.6", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-FbCKyr5KDiPULUzN/nm5oqQs9nXCHD8dVc64BArxJadCvbNzAI6lUWGh9fSJZWeDIRD38ikceBU8Kj/Uh+53oQ=="],
|
||||
|
||||
"jose": ["jose@6.0.10", "", {}, "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw=="],
|
||||
|
||||
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
}
|
||||
}
|
||||
23
content/blogs/phantom-token/api/index.ts
Normal file
23
content/blogs/phantom-token/api/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createRemoteJWKSet, jwtVerify } from "jose";
|
||||
|
||||
const jwks = createRemoteJWKSet(
|
||||
new URL(
|
||||
".well-known/jwks.json",
|
||||
process.env.AUTH_SERVER ?? "http://auth:8080",
|
||||
),
|
||||
);
|
||||
|
||||
Bun.serve({
|
||||
routes: {
|
||||
"/api/me": async (req) => {
|
||||
const auth = req.headers.get("authorization");
|
||||
const bearer = auth?.slice(7);
|
||||
console.log("auth", auth, "bearer", bearer);
|
||||
if (!bearer) return new Response("Forbidden", { status: 403 });
|
||||
|
||||
const { payload } = await jwtVerify(bearer, jwks);
|
||||
console.log("success", "jwt payload", payload);
|
||||
return new Response(JSON.stringify(payload));
|
||||
},
|
||||
},
|
||||
});
|
||||
15
content/blogs/phantom-token/api/package.json
Normal file
15
content/blogs/phantom-token/api/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "api",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"jose": "^6.0.10"
|
||||
}
|
||||
}
|
||||
27
content/blogs/phantom-token/api/tsconfig.json
Normal file
27
content/blogs/phantom-token/api/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
12
content/blogs/phantom-token/auth/Dockerfile
Normal file
12
content/blogs/phantom-token/auth/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM golang:1.24
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /keibi
|
||||
|
||||
EXPOSE 8080
|
||||
CMD ["/keibi"]
|
||||
|
||||
20
content/blogs/phantom-token/auth/go.mod
Normal file
20
content/blogs/phantom-token/auth/go.mod
Normal file
@@ -0,0 +1,20 @@
|
||||
module github.com/zoriya/kyoo/blog/phantom-token
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/lestrrat-go/jwx v1.2.30
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
)
|
||||
37
content/blogs/phantom-token/auth/go.sum
Normal file
37
content/blogs/phantom-token/auth/go.sum
Normal file
@@ -0,0 +1,37 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
|
||||
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||
github.com/lestrrat-go/jwx v1.2.30 h1:VKIFrmjYn0z2J51iLPadqoHIVLzvWNa1kCsTqNDHYPA=
|
||||
github.com/lestrrat-go/jwx v1.2.30/go.mod h1:vMxrwFhunGZ3qddmfmEm2+uced8MSI6QFWGTKygjSzQ=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
113
content/blogs/phantom-token/auth/main.go
Normal file
113
content/blogs/phantom-token/auth/main.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/lestrrat-go/jwx/jwk"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
rsakey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
users := make(map[string]User)
|
||||
|
||||
http.HandleFunc("/auth/user", func(w http.ResponseWriter, r *http.Request) {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var u User
|
||||
err := decoder.Decode(&u)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// generate random session id
|
||||
id := make([]byte, 64)
|
||||
_, err = rand.Read(id)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
token := base64.StdEncoding.EncodeToString(id)
|
||||
|
||||
users[token] = u
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(struct {
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token"`
|
||||
}{Name: u.Name, Token: token})
|
||||
})
|
||||
|
||||
http.HandleFunc("/auth/jwt", func(w http.ResponseWriter, r *http.Request) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
http.Error(w, "Missing session token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
token := auth[len("Bearer "):]
|
||||
|
||||
user, ok := users[token]
|
||||
if !ok {
|
||||
http.Error(w, "Invalid token", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
claims := make(jwt.MapClaims)
|
||||
claims["sub"] = user.Name
|
||||
claims["iss"] = "keibi-blog"
|
||||
claims["iat"] = &jwt.NumericDate{
|
||||
Time: time.Now().UTC(),
|
||||
}
|
||||
claims["exp"] = &jwt.NumericDate{
|
||||
Time: time.Now().UTC().Add(time.Hour),
|
||||
}
|
||||
jwt := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
t, err := jwt.SignedString(rsakey)
|
||||
if err != nil {
|
||||
http.Error(w, "Could not sign token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Authorization", fmt.Sprintf("Bearer %s", t))
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(jwt)
|
||||
})
|
||||
|
||||
http.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
key, err := jwk.New(rsakey.PublicKey)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
key.Set("use", "sig")
|
||||
key.Set("key_ops", "verify")
|
||||
set := jwk.NewSet()
|
||||
set.Add(key)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(set)
|
||||
})
|
||||
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
36
content/blogs/phantom-token/docker-compose.yaml
Normal file
36
content/blogs/phantom-token/docker-compose.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
services:
|
||||
auth:
|
||||
build: ./auth
|
||||
restart: on-failure
|
||||
ports:
|
||||
- "8080:8080"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.auth.rule=PathPrefix(`/auth/`) || PathPrefix(`/.well-known/`)"
|
||||
|
||||
api:
|
||||
build: ./api
|
||||
restart: on-failure
|
||||
ports:
|
||||
- "3000:3000"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.api.rule=PathPrefix(`/api/`)"
|
||||
- "traefik.http.routers.api.middlewares=phantom-token"
|
||||
- "traefik.http.middlewares.phantom-token.forwardauth.address=http://auth:8080/auth/jwt"
|
||||
- "traefik.http.middlewares.phantom-token.forwardauth.authRequestHeaders=Authorization,X-Api-Key"
|
||||
- "traefik.http.middlewares.phantom-token.forwardauth.authResponseHeaders=Authorization"
|
||||
|
||||
traefik:
|
||||
image: traefik:v3.3
|
||||
restart: on-failure
|
||||
command:
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entryPoints.web.address=:7891"
|
||||
- "--accesslog=true"
|
||||
ports:
|
||||
- "7891:7891"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
|
||||
9
content/blogs/phantom-token/index.md
Normal file
9
content/blogs/phantom-token/index.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "Phantom token"
|
||||
description: ""
|
||||
date: 2025-03-26
|
||||
draft: true
|
||||
tags: []
|
||||
---
|
||||
|
||||
|
||||
7
content/blogs/phantom-token/shell.nix
Normal file
7
content/blogs/phantom-token/shell.nix
Normal file
@@ -0,0 +1,7 @@
|
||||
{pkgs ? import <nixpkgs> {}}:
|
||||
pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
bun
|
||||
go
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user