diff --git a/README.md b/README.md
index ac2f2a5b5..b3bcd7ac6 100644
--- a/README.md
+++ b/README.md
@@ -270,13 +270,13 @@ We're currently setting the foundation and building [integrations](https://infis
|
-
- ✔️ Ruby on Rails
+
+ ✔️ Vue
|
-
- ✔️ Vue
+
+ ✔️ Ruby on Rails
|
@@ -292,6 +292,16 @@ We're currently setting the foundation and building [integrations](https://infis
+
+ |
+
+ ✔️ .NET
+
+ |
+
+ And more...
+ |
+
diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts
index d36e32b91..bbaaff8c3 100644
--- a/backend/src/models/secret.ts
+++ b/backend/src/models/secret.ts
@@ -33,7 +33,8 @@ const secretSchema = new Schema(
{
version: {
type: Number,
- required: true
+ required: true,
+ default: 1
},
workspace: {
type: Schema.Types.ObjectId,
diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts
index 17a91d39c..fb2a23f42 100644
--- a/backend/src/routes/v2/secret.ts
+++ b/backend/src/routes/v2/secret.ts
@@ -1,4 +1,137 @@
-import express from 'express';
+import express, { Request, Response } from 'express';
+import { requireAuth, validateRequest } from '../../middleware';
+import { ISecret, Secret } from '../../models';
+import { decryptSymmetric } from '../../utils/crypto';
+import { getLogger } from '../../utils/logger';
+import { body, param, query, check } from 'express-validator';
+import { BadRequestError } from '../../utils/errors';
const router = express.Router();
+/**
+ * Create a single secret for a given workspace and environment
+ */
+router.post(
+ '/', requireAuth,
+ body('secret').exists().isObject(),
+ async (req: Request, res: Response) => {
+ try {
+ const { secret }: { secret: ISecret[] } = req.body;
+ const newlyCreatedSecret = await Secret.create(secret)
+ res.status(200).json(newlyCreatedSecret)
+ } catch {
+ throw BadRequestError({ message: "Unable to create the secret" })
+ }
+ }
+);
+
+/**
+ * Create many secrets
+ */
+router.post(
+ '/bulk-create', requireAuth,
+ body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')),
+ async (req: Request, res: Response) => {
+ try {
+ const { secrets }: { secrets: ISecret[] } = req.body;
+ const newlyCreatedSecrets = await Secret.insertMany(secrets)
+ res.status(200).json(newlyCreatedSecrets)
+ } catch {
+ throw BadRequestError({ message: "Unable to create the secret" })
+ }
+ }
+);
+
+/**
+ * Get a single secret by secret id
+ */
+router.get(
+ '/:secretId', requireAuth, param('secretId').exists().trim(),
+ validateRequest, async (req: Request, res: Response) => {
+ try {
+ const secretFromDB = await Secret.findById(req.params.secretId)
+ return res.status(200).send(secretFromDB);
+ } catch (e) {
+ throw BadRequestError({ message: "Unable to find the requested secret" })
+ }
+ }
+);
+
+/**
+ * Get a single secret by secret id
+ */
+router.get(
+ '/:bulk', requireAuth, param('secretId').exists().trim(),
+ validateRequest, async (req: Request, res: Response) => {
+ try {
+ const secretFromDB = await Secret.findById(req.params.secretId)
+ return res.status(200).send(secretFromDB);
+ } catch (e) {
+ throw BadRequestError({ message: "Unable to find the requested secret" })
+ }
+ }
+);
+
+/**
+ * Delete a single secret by secret id
+ */
+router.delete(
+ '/:secretId',
+ requireAuth,
+ param('secretId').exists().trim(),
+ validateRequest, async (req: Request, res: Response) => {
+ try {
+ const secretFromDB = await Secret.deleteOne({
+ _id: req.params.secretId
+ })
+ return res.status(200).send(secretFromDB);
+ } catch (e) {
+ throw BadRequestError({ message: "Unable to find the requested secret" })
+ }
+ }
+);
+
+/**
+ * Delete many secrets by secret ids
+ */
+router.delete(
+ '/batch',
+ requireAuth,
+ body('secretIds').exists().isArray(),
+ validateRequest, async (req: Request, res: Response) => {
+ try {
+ const secretIdsToDelete: string[] = req.body.secretIds
+ const secretFromDB = await Secret.deleteMany({
+ _id: { $in: secretIdsToDelete }
+ })
+ return res.status(200).send(secretFromDB);
+ } catch (error) {
+ throw BadRequestError({ message: `Unable to delete the requested secrets by ids [${req.body.secretIds}]` })
+ }
+ }
+);
+
+/**
+ * Apply modifications to many existing secrets
+ */
+router.patch(
+ '/bulk-update',
+ requireAuth,
+ body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')),
+ validateRequest, async (req: Request, res: Response) => {
+ try {
+ const { secrets }: { secrets: ISecret[] } = req.body;
+
+ const operations = secrets.map((secretToUpdate: ISecret) => ({
+ updateOne: { filter: { _id: secretToUpdate._id }, update: secretToUpdate },
+ }));
+
+ const bulkModificationInfo = await Secret.bulkWrite(operations);
+
+ return res.status(200).json(bulkModificationInfo)
+ } catch (error) {
+ throw BadRequestError({ message: `Unable to process the bulk update. Double check the ids of the secrets` })
+ }
+ }
+);
+
export default router;
diff --git a/cli/go.mod b/cli/go.mod
index c48e3e2f9..956e9bb29 100644
--- a/cli/go.mod
+++ b/cli/go.mod
@@ -13,6 +13,7 @@ require (
require (
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
+ github.com/Luzifer/go-openssl/v4 v4.1.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
@@ -34,6 +35,7 @@ require (
)
require (
+ github.com/Luzifer/go-openssl v2.0.0+incompatible
github.com/go-resty/resty/v2 v2.7.0
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jedib0t/go-pretty v4.3.0+incompatible
diff --git a/cli/go.sum b/cli/go.sum
index d169d7b89..2b8836515 100644
--- a/cli/go.sum
+++ b/cli/go.sum
@@ -2,6 +2,10 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMb
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk=
+github.com/Luzifer/go-openssl v2.0.0+incompatible h1:EpNNxrPDji4rRzE0KeOeIeV7pHyKe8zF9oNnAXy4mBY=
+github.com/Luzifer/go-openssl v2.0.0+incompatible/go.mod h1:t2qnLjT8WQ3usGU1R8uAqjY4T7CK7eMg9vhQ3l9Ue/Y=
+github.com/Luzifer/go-openssl/v4 v4.1.0 h1:8qi3Z6f8Aflwub/Cs4FVSmKUEg/lC8GlODbR2TyZ+nM=
+github.com/Luzifer/go-openssl/v4 v4.1.0/go.mod h1:3i1T3Pe6eQK19d86WhuQzjLyMwBaNmGmt3ZceWpWVa4=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -96,15 +100,20 @@ github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgk
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg=
go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -116,6 +125,7 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go
new file mode 100644
index 000000000..f8efd6724
--- /dev/null
+++ b/cli/packages/cmd/secrets.go
@@ -0,0 +1,103 @@
+/*
+Copyright © 2022 NAME HERE
+*/
+package cmd
+
+import (
+ "fmt"
+
+ "github.com/Infisical/infisical-merge/packages/models"
+ "github.com/Infisical/infisical-merge/packages/util"
+ "github.com/Infisical/infisical-merge/packages/visualize"
+ log "github.com/sirupsen/logrus"
+ "github.com/spf13/cobra"
+)
+
+var secretsCmd = &cobra.Command{
+ Example: `infisical secrets"`,
+ Short: "Used to create, read update and delete secrets",
+ Use: "secrets",
+ DisableFlagsInUseLine: true,
+ PreRun: toggleDebug,
+ Args: cobra.NoArgs,
+ Run: func(cmd *cobra.Command, args []string) {
+
+ secrets, err := util.GetAllEnvironmentVariables("", "dev")
+ secrets = util.SubstituteSecrets(secrets)
+ if err != nil {
+ log.Debugln(err)
+ return
+ }
+ visualize.PrintAllSecretDetails(secrets)
+ },
+}
+
+var secretsGetCmd = &cobra.Command{
+ Example: `secrets get ..."`,
+ Short: "Used to retrieve secrets by name",
+ Use: "get [secrets]",
+ DisableFlagsInUseLine: true,
+ Args: cobra.MinimumNArgs(1),
+ PreRun: toggleDebug,
+ Run: getSecretsByNames,
+}
+
+var secretsSetCmd = &cobra.Command{
+ Example: `secrets set ..."`,
+ Short: "Used update retrieve secrets by name",
+ Use: "set [secrets]",
+ DisableFlagsInUseLine: true,
+ PreRun: toggleDebug,
+ Args: cobra.NoArgs,
+ Run: func(cmd *cobra.Command, args []string) {
+ fmt.Println("set secret")
+ },
+}
+
+var secretsDeleteCmd = &cobra.Command{
+ Example: `secrets delete ..."`,
+ Short: "Used to delete secrets by name",
+ Use: "delete [secrets]",
+ DisableFlagsInUseLine: true,
+ PreRun: toggleDebug,
+ Args: cobra.NoArgs,
+ Run: func(cmd *cobra.Command, args []string) {
+ fmt.Println("Delete secret")
+ },
+}
+
+func init() {
+ secretsCmd.AddCommand(secretsGetCmd)
+ secretsCmd.AddCommand(secretsSetCmd)
+ secretsCmd.AddCommand(secretsDeleteCmd)
+ rootCmd.AddCommand(secretsCmd)
+}
+
+func getSecretsByNames(cmd *cobra.Command, args []string) {
+ secrets, err := util.GetAllEnvironmentVariables("", "dev")
+ if err != nil {
+ log.Error("Unable to retrieve secrets. Run with -d to see full logs")
+ log.Debug(err)
+ }
+
+ requestedSecrets := []models.SingleEnvironmentVariable{}
+
+ secretsMap := make(map[string]models.SingleEnvironmentVariable)
+ for _, secret := range secrets {
+ secretsMap[secret.Key] = secret
+ }
+
+ for _, secretKeyFromArg := range args {
+ if value, ok := secretsMap[secretKeyFromArg]; ok {
+ requestedSecrets = append(requestedSecrets, value)
+ } else {
+ requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{
+ Key: secretKeyFromArg,
+ Type: "NOT FOUND",
+ Value: "NOT FOUND",
+ })
+ }
+ }
+
+ visualize.PrintAllSecretDetails(requestedSecrets)
+}
diff --git a/cli/packages/util/crypto.go b/cli/packages/util/crypto.go
index c6eee2d0c..0431989de 100644
--- a/cli/packages/util/crypto.go
+++ b/cli/packages/util/crypto.go
@@ -3,21 +3,26 @@ package util
import (
"crypto/aes"
"crypto/cipher"
+ "crypto/rand"
+ "io"
+
+ "golang.org/x/crypto/nacl/box"
)
-func DecryptSymmetric(key []byte, encryptedPrivateKey []byte, tag []byte, IV []byte) ([]byte, error) {
+// will decrypt cipher text to plain text using iv and tag
+func DecryptSymmetric(key []byte, cipherText []byte, tag []byte, iv []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
- aesgcm, err := cipher.NewGCMWithNonceSize(block, len(IV))
+ aesgcm, err := cipher.NewGCMWithNonceSize(block, len(iv))
if err != nil {
return nil, err
}
- var nonce = IV
- var ciphertext = append(encryptedPrivateKey, tag...)
+ var nonce = iv
+ var ciphertext = append(cipherText, tag...) // the aesgcm open method expects auth tag at the end of the cipher text
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
@@ -26,3 +31,46 @@ func DecryptSymmetric(key []byte, encryptedPrivateKey []byte, tag []byte, IV []b
return plaintext, nil
}
+
+func GenerateNewKey() (newKey []byte, keyErr error) {
+ key := make([]byte, 16) // block size defaults to 16 so this is fine
+ _, err := rand.Read(key)
+ return key, err
+}
+
+// Will encrypt a plain text with the provided key
+func EncryptSymmetric(plaintext []byte, key []byte) (cipherText []byte, iv []byte, tag []byte, err error) {
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ aesgcm, err := cipher.NewGCMWithNonceSize(block, 16) // default is 12, 16 because https://github.com/Infisical/infisical/blob/bea0ff6e05a4de73a5db625d4ae181a015b50855/backend/src/utils/aes-gcm.ts#L4
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ // create a nonce
+ nonce := make([]byte, aesgcm.NonceSize())
+ if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
+ panic(err)
+ }
+
+ ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)
+
+ ciphertextOnly := ciphertext[:len(ciphertext)-16] // combines the auth tag with the cipher text so we need to extract it
+
+ authTag := ciphertext[len(ciphertext)-16:]
+
+ return ciphertextOnly, nonce, authTag, nil
+}
+
+func DecryptAsymmetric(ciphertext []byte, nonce []byte, publicKey []byte, privateKey []byte) (plainText []byte) {
+ plainTextToReturn, _ := box.Open(nil, ciphertext, (*[24]byte)(nonce), (*[32]byte)(publicKey), (*[32]byte)(privateKey))
+ return plainTextToReturn
+}
+
+func EncryptAssymmetric(message []byte, nonce []byte, publicKey []byte, privateKey []byte) (encryptedMessage []byte) {
+ encryptedPlainText := box.Seal(nil, message, (*[24]byte)(nonce), (*[32]byte)(publicKey), (*[32]byte)(privateKey))
+ return encryptedPlainText
+}
diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go
index f88aed96b..f41e73a0f 100644
--- a/cli/packages/util/secrets.go
+++ b/cli/packages/util/secrets.go
@@ -11,7 +11,6 @@ import (
"github.com/Infisical/infisical-merge/packages/models"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
- "golang.org/x/crypto/nacl/box"
)
const PERSONAL_SECRET_TYPE_NAME = "personal"
@@ -56,7 +55,7 @@ func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string,
}
// log.Debugln("workspaceKey", workspaceKey, "nonce", nonce, "senderPublicKey", senderPublicKey, "currentUsersPrivateKey", currentUsersPrivateKey)
- workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey))
+ workspaceKeyInBytes := DecryptAsymmetric(workspaceKey, nonce, senderPublicKey, currentUsersPrivateKey)
var listOfEnv []models.SingleEnvironmentVariable
for _, secret := range pullSecretsRequestResponse.Secrets {
@@ -166,7 +165,8 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string,
return nil, err
}
- workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey))
+ // workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey))
+ workspaceKeyInBytes := DecryptAsymmetric(workspaceKey, nonce, senderPublicKey, currentUsersPrivateKey)
var listOfEnv []models.SingleEnvironmentVariable
for _, secret := range pullSecretsByInfisicalTokenResponse.Secrets {
@@ -223,6 +223,7 @@ func GetAllEnvironmentVariables(projectId string, envName string) ([]models.Sing
return nil, err
}
+ // TODO: Should be based on flag. I.e only get all workspaces if desired, otherwise only get the one in the current root of project
workspaceConfigs, err := GetAllWorkSpaceConfigsStartingFromCurrentPath()
if err != nil {
return nil, fmt.Errorf("unable to check if you have a %s file in your current directory", INFISICAL_WORKSPACE_CONFIG_FILE_NAME)
diff --git a/cli/packages/visualize/secrets.go b/cli/packages/visualize/secrets.go
new file mode 100644
index 000000000..e732d06af
--- /dev/null
+++ b/cli/packages/visualize/secrets.go
@@ -0,0 +1,14 @@
+package visualize
+
+import "github.com/Infisical/infisical-merge/packages/models"
+
+func PrintAllSecretDetails(secrets []models.SingleEnvironmentVariable) {
+ rows := [][]string{}
+ for _, secret := range secrets {
+ rows = append(rows, []string{secret.Key, secret.Value, secret.Type})
+ }
+
+ headers := []string{"Secret name", "Secret vaule", "Secret type"}
+
+ Table(headers, rows)
+}
diff --git a/cli/packages/visualize/visualize.go b/cli/packages/visualize/visualize.go
index 85d946a32..5658197ee 100644
--- a/cli/packages/visualize/visualize.go
+++ b/cli/packages/visualize/visualize.go
@@ -6,13 +6,23 @@ import (
"github.com/jedib0t/go-pretty/table"
)
+type TableOptions struct {
+ Title string
+}
+
+// func GetDefaultTableOptions() TableOptions{
+// return TableOptions{
+// Title: "",
+// }
+// }
+
// Given headers and rows, this function will print out a table
func Table(headers []string, rows [][]string) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.SetStyle(table.StyleLight)
- // t.SetTitle("Title")
+ // t.SetTitle(tableOptions.Title)
t.Style().Options.DrawBorder = true
t.Style().Options.SeparateHeader = true
t.Style().Options.SeparateColumns = true
diff --git a/docs/mint.json b/docs/mint.json
index cbac56d5a..e94b70a6b 100644
--- a/docs/mint.json
+++ b/docs/mint.json
@@ -166,7 +166,8 @@
"integrations/frameworks/django",
"integrations/frameworks/flask",
"integrations/frameworks/laravel",
- "integrations/frameworks/rails"
+ "integrations/frameworks/rails",
+ "integrations/frameworks/dotnet"
]
},
{