1package main
2
3import (
4 "crypto/ecdsa"
5 "crypto/elliptic"
6 "crypto/rand"
7 "encoding/json"
8 "fmt"
9 "os"
10 "time"
11
12 "github.com/bluesky-social/indigo/atproto/crypto"
13 "github.com/bluesky-social/indigo/atproto/syntax"
14 "github.com/haileyok/cocoon/internal/helpers"
15 "github.com/lestrrat-go/jwx/v2/jwk"
16 "github.com/urfave/cli/v2"
17 "golang.org/x/crypto/bcrypt"
18 "gorm.io/driver/sqlite"
19 "gorm.io/gorm"
20)
21
22func main() {
23 app := cli.App{
24 Name: "admin",
25 Commands: cli.Commands{
26 runCreateRotationKey,
27 runCreatePrivateJwk,
28 runCreateInviteCode,
29 runResetPassword,
30 },
31 ErrWriter: os.Stdout,
32 }
33
34 app.Run(os.Args)
35}
36
37var runCreateRotationKey = &cli.Command{
38 Name: "create-rotation-key",
39 Usage: "creates a rotation key for your pds",
40 Flags: []cli.Flag{
41 &cli.StringFlag{
42 Name: "out",
43 Required: true,
44 Usage: "output file for your rotation key",
45 },
46 },
47 Action: func(cmd *cli.Context) error {
48 key, err := crypto.GeneratePrivateKeyK256()
49 if err != nil {
50 return err
51 }
52
53 bytes := key.Bytes()
54
55 if err := os.WriteFile(cmd.String("out"), bytes, 0644); err != nil {
56 return err
57 }
58
59 return nil
60 },
61}
62
63var runCreatePrivateJwk = &cli.Command{
64 Name: "create-private-jwk",
65 Usage: "creates a private jwk for your pds",
66 Flags: []cli.Flag{
67 &cli.StringFlag{
68 Name: "out",
69 Required: true,
70 Usage: "output file for your jwk",
71 },
72 },
73 Action: func(cmd *cli.Context) error {
74 privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
75 if err != nil {
76 return err
77 }
78
79 key, err := jwk.FromRaw(privKey)
80 if err != nil {
81 return err
82 }
83
84 kid := fmt.Sprintf("%d", time.Now().Unix())
85
86 if err := key.Set(jwk.KeyIDKey, kid); err != nil {
87 return err
88 }
89
90 b, err := json.Marshal(key)
91 if err != nil {
92 return err
93 }
94
95 if err := os.WriteFile(cmd.String("out"), b, 0644); err != nil {
96 return err
97 }
98
99 return nil
100 },
101}
102
103var runCreateInviteCode = &cli.Command{
104 Name: "create-invite-code",
105 Usage: "creates an invite code",
106 Flags: []cli.Flag{
107 &cli.StringFlag{
108 Name: "for",
109 Usage: "optional did to assign the invite code to",
110 },
111 &cli.IntFlag{
112 Name: "uses",
113 Usage: "number of times the invite code can be used",
114 Value: 1,
115 },
116 },
117 Action: func(cmd *cli.Context) error {
118 db, err := newDb()
119 if err != nil {
120 return err
121 }
122
123 forDid := "did:plc:123"
124 if cmd.String("for") != "" {
125 did, err := syntax.ParseDID(cmd.String("for"))
126 if err != nil {
127 return err
128 }
129
130 forDid = did.String()
131 }
132
133 uses := cmd.Int("uses")
134
135 code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(8), helpers.RandomVarchar(8))
136
137 if err := db.Exec("INSERT INTO invite_codes (did, code, remaining_use_count) VALUES (?, ?, ?)", forDid, code, uses).Error; err != nil {
138 return err
139 }
140
141 fmt.Printf("New invite code created with %d uses: %s\n", uses, code)
142
143 return nil
144 },
145}
146
147var runResetPassword = &cli.Command{
148 Name: "reset-password",
149 Usage: "resets a password",
150 Flags: []cli.Flag{
151 &cli.StringFlag{
152 Name: "did",
153 Usage: "did of the user who's password you want to reset",
154 },
155 },
156 Action: func(cmd *cli.Context) error {
157 db, err := newDb()
158 if err != nil {
159 return err
160 }
161
162 didStr := cmd.String("did")
163 did, err := syntax.ParseDID(didStr)
164 if err != nil {
165 return err
166 }
167
168 newPass := fmt.Sprintf("%s-%s", helpers.RandomVarchar(12), helpers.RandomVarchar(12))
169 hashed, err := bcrypt.GenerateFromPassword([]byte(newPass), 10)
170 if err != nil {
171 return err
172 }
173
174 if err := db.Exec("UPDATE repos SET password = ? WHERE did = ?", hashed, did.String()).Error; err != nil {
175 return err
176 }
177
178 fmt.Printf("Password for %s has been reset to: %s", did.String(), newPass)
179
180 return nil
181 },
182}
183
184func newDb() (*gorm.DB, error) {
185 return gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{})
186}