Skip to content

Commit 7cab85b

Browse files
committed
feat(pro): add subscription mocks
1 parent adf2d2a commit 7cab85b

File tree

9 files changed

+127
-15
lines changed

9 files changed

+127
-15
lines changed

api/helpers/helpers.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ func Store(r *http.Request) db.Store {
1212
return GetFromContext(r, "store").(db.Store)
1313
}
1414

15+
func TerraformStore(r *http.Request) db.TerraformStore {
16+
return GetFromContext(r, "store").(db.TerraformStore)
17+
}
18+
1519
func isXHR(w http.ResponseWriter, r *http.Request) bool {
1620
accept := r.Header.Get("Accept")
1721
return !strings.Contains(accept, "text/html")

api/login.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,26 @@ func tryFindLDAPUser(username, password string) (*db.User, error) {
151151
// createSession creates session for passed user and stores session details
152152
// in cookies.
153153
func createSession(w http.ResponseWriter, r *http.Request, user db.User, oidc bool) {
154+
var err error
154155
var verificationMethod db.SessionVerificationMethod
155156
verified := false
157+
156158
switch {
157159
case user.Totp != nil && util.Config.Auth.Totp.Enabled:
158160
verificationMethod = db.SessionVerificationTotp
161+
case util.Config.Auth.Email.Enabled && (!util.Config.Auth.Email.DisableForOidc || !oidc):
162+
code := random.Number(6)
163+
_, err = helpers.Store(r).AddEmailOtpVerification(user.ID, code)
164+
if err != nil {
165+
log.WithError(err).WithFields(log.Fields{
166+
"user_id": user.ID,
167+
"context": "session",
168+
}).Error("Failed to add email otp verification")
169+
helpers.WriteErrorStatus(w, "Failed to create email OTP verification", http.StatusInternalServerError)
170+
return
171+
}
172+
173+
verificationMethod = db.SessionVerificationEmail
159174
default:
160175
verificationMethod = db.SessionVerificationNone
161176
verified = true
@@ -171,8 +186,12 @@ func createSession(w http.ResponseWriter, r *http.Request, user db.User, oidc bo
171186
VerificationMethod: verificationMethod,
172187
Verified: verified,
173188
})
189+
174190
if err != nil {
175-
log.Error(err)
191+
log.WithError(err).WithFields(log.Fields{
192+
"user_id": user.ID,
193+
"context": "session",
194+
}).Error("Failed to create session")
176195
helpers.WriteErrorStatus(w, "Failed to create session", http.StatusInternalServerError)
177196
return
178197
}
@@ -714,6 +733,7 @@ func oidcRedirect(w http.ResponseWriter, r *http.Request) {
714733
Name: claims.name,
715734
Email: claims.email,
716735
External: true,
736+
Pro: true,
717737
}
718738
user, err = helpers.Store(r).CreateUserWithoutPassword(user)
719739
if err != nil {

api/router.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"embed"
66
"fmt"
7+
"github.com/semaphoreui/semaphore/services/interfaces"
78
"net/http"
89
"os"
910
"path"
@@ -91,6 +92,7 @@ func Route(
9192
secretStorageService server.SecretStorageService,
9293
accessKeyService server.AccessKeyService,
9394
environmentService server.EnvironmentService,
95+
subscriptionService interfaces.SubscriptionService,
9496
) *mux.Router {
9597

9698
projectController := &projects.ProjectController{ProjectService: projectService}
@@ -102,6 +104,8 @@ func Route(
102104
keyController := projects.NewKeyController(accessKeyService)
103105
projectsController := projects.NewProjectsController(accessKeyService)
104106
terraformController := proApi.NewTerraformController(encryptionService, terraformStore)
107+
userController := NewUserController(subscriptionService)
108+
usersController := NewUsersController(subscriptionService)
105109

106110
r := mux.NewRouter()
107111
r.NotFoundHandler = http.HandlerFunc(servePublic)
@@ -182,9 +186,9 @@ func Route(
182186
authenticatedAPI.Path("/events").HandlerFunc(getAllEvents).Methods("GET", "HEAD")
183187
authenticatedAPI.HandleFunc("/events/last", getLastEvents).Methods("GET", "HEAD")
184188

185-
authenticatedAPI.Path("/users").HandlerFunc(getUsers).Methods("GET", "HEAD")
186-
authenticatedAPI.Path("/users").HandlerFunc(addUser).Methods("POST")
187-
authenticatedAPI.Path("/user").HandlerFunc(getUser).Methods("GET", "HEAD")
189+
authenticatedAPI.Path("/users").HandlerFunc(usersController.GetUsers).Methods("GET", "HEAD")
190+
authenticatedAPI.Path("/users").HandlerFunc(usersController.AddUser).Methods("POST")
191+
authenticatedAPI.Path("/user").HandlerFunc(userController.GetUser).Methods("GET", "HEAD")
188192

189193
authenticatedAPI.Path("/apps").HandlerFunc(getApps).Methods("GET", "HEAD")
190194

@@ -230,12 +234,12 @@ func Route(
230234

231235
userUserAPI := authenticatedAPI.Path("/users/{user_id}").Subrouter()
232236
userUserAPI.Use(readonlyUserMiddleware)
233-
userUserAPI.Methods("GET", "HEAD").HandlerFunc(getUser)
237+
userUserAPI.Methods("GET", "HEAD").HandlerFunc(userController.GetUser)
234238

235239
userAPI := authenticatedAPI.Path("/users/{user_id}").Subrouter()
236240
userAPI.Use(getUserMiddleware)
237241

238-
userAPI.Methods("PUT").HandlerFunc(updateUser)
242+
userAPI.Methods("PUT").HandlerFunc(usersController.UpdateUser)
239243
userAPI.Methods("DELETE").HandlerFunc(deleteUser)
240244

241245
userPasswordAPI := authenticatedAPI.PathPrefix("/users/{user_id}").Subrouter()

api/user.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,38 @@ import (
66
"github.com/gorilla/mux"
77
"github.com/semaphoreui/semaphore/api/helpers"
88
"github.com/semaphoreui/semaphore/db"
9+
"github.com/semaphoreui/semaphore/services/interfaces"
910
"github.com/semaphoreui/semaphore/util"
1011
"io"
1112
"net/http"
1213
"strings"
1314
)
1415

15-
func getUser(w http.ResponseWriter, r *http.Request) {
16+
type UserController struct {
17+
subscriptionService interfaces.SubscriptionService
18+
}
19+
20+
func NewUserController(subscriptionService interfaces.SubscriptionService) *UserController {
21+
return &UserController{
22+
subscriptionService: subscriptionService,
23+
}
24+
}
25+
26+
func (c *UserController) GetUser(w http.ResponseWriter, r *http.Request) {
1627
if u, exists := helpers.GetOkFromContext(r, "_user"); exists {
1728
helpers.WriteJSON(w, http.StatusOK, u)
1829
return
1930
}
2031

2132
var user struct {
2233
db.User
23-
CanCreateProject bool `json:"can_create_project"`
34+
CanCreateProject bool `json:"can_create_project"`
35+
HasActiveSubscription bool `json:"has_active_subscription"`
2436
}
2537

2638
user.User = *helpers.GetFromContext(r, "user").(*db.User)
2739
user.CanCreateProject = user.Admin || util.Config.NonAdminCanCreateProject
40+
user.HasActiveSubscription = c.subscriptionService.HasActiveSubscription()
2841

2942
helpers.WriteJSON(w, http.StatusOK, user)
3043
}

api/users.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,36 @@ package api
22

33
import (
44
"bytes"
5+
"fmt"
56
"github.com/pquerna/otp"
67
"github.com/pquerna/otp/totp"
78
"github.com/semaphoreui/semaphore/api/helpers"
89
"github.com/semaphoreui/semaphore/db"
10+
"github.com/semaphoreui/semaphore/services/interfaces"
911
log "github.com/sirupsen/logrus"
1012
"image/png"
1113
"net/http"
1214

1315
"github.com/semaphoreui/semaphore/util"
1416
)
1517

18+
type UsersController struct {
19+
subscriptionService interfaces.SubscriptionService
20+
}
21+
22+
func NewUsersController(subscriptionService interfaces.SubscriptionService) *UsersController {
23+
return &UsersController{
24+
subscriptionService: subscriptionService,
25+
}
26+
}
27+
1628
type minimalUser struct {
1729
ID int `json:"id"`
1830
Username string `json:"username"`
1931
Name string `json:"name"`
2032
}
2133

22-
func getUsers(w http.ResponseWriter, r *http.Request) {
34+
func (c *UsersController) GetUsers(w http.ResponseWriter, r *http.Request) {
2335
currentUser := helpers.GetFromContext(r, "user").(*db.User)
2436
users, err := helpers.Store(r).GetUsers(db.RetrieveQueryParams{
2537
Filter: r.URL.Query().Get("s"),
@@ -46,7 +58,7 @@ func getUsers(w http.ResponseWriter, r *http.Request) {
4658
}
4759
}
4860

49-
func addUser(w http.ResponseWriter, r *http.Request) {
61+
func (c *UsersController) AddUser(w http.ResponseWriter, r *http.Request) {
5062
var user db.UserWithPwd
5163
if !helpers.Bind(w, r, &user) {
5264
return
@@ -59,6 +71,21 @@ func addUser(w http.ResponseWriter, r *http.Request) {
5971
return
6072
}
6173

74+
if user.Pro {
75+
ok, err := c.subscriptionService.CanAddProUser(helpers.Store(r))
76+
77+
if err != nil {
78+
w.WriteHeader(http.StatusInternalServerError)
79+
return
80+
}
81+
82+
if !ok {
83+
helpers.WriteErrorStatus(w,
84+
fmt.Sprintf("You have reached the limit of Pro users for your subscription."), http.StatusForbidden)
85+
return
86+
}
87+
}
88+
6289
var err error
6390
var newUser db.User
6491

@@ -134,7 +161,7 @@ func getUserMiddleware(next http.Handler) http.Handler {
134161
})
135162
}
136163

137-
func updateUser(w http.ResponseWriter, r *http.Request) {
164+
func (c *UsersController) UpdateUser(w http.ResponseWriter, r *http.Request) {
138165
targetUser := helpers.GetFromContext(r, "_user").(db.User)
139166
editor := helpers.GetFromContext(r, "user").(*db.User)
140167

@@ -149,6 +176,21 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
149176
return
150177
}
151178

179+
if user.Pro {
180+
ok, err := c.subscriptionService.CanAddProUser(helpers.Store(r))
181+
182+
if err != nil {
183+
w.WriteHeader(http.StatusInternalServerError)
184+
return
185+
}
186+
187+
if !ok {
188+
helpers.WriteErrorStatus(w,
189+
fmt.Sprintf("You have reached the limit of Pro users for your subscription."), http.StatusForbidden)
190+
return
191+
}
192+
}
193+
152194
if !editor.Admin && editor.ID != targetUser.ID {
153195
log.Warn(editor.Username + " is not permitted to edit users")
154196
w.WriteHeader(http.StatusUnauthorized)

cli/cmd/root.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/semaphoreui/semaphore/db"
1616
"github.com/semaphoreui/semaphore/db/factory"
1717
proFactory "github.com/semaphoreui/semaphore/pro/db/factory"
18+
proServer "github.com/semaphoreui/semaphore/pro/services/server"
1819
"github.com/semaphoreui/semaphore/services/schedules"
1920
"github.com/semaphoreui/semaphore/services/tasks"
2021
"github.com/semaphoreui/semaphore/util"
@@ -85,6 +86,7 @@ func runService() {
8586
accessKeyService := server.NewAccessKeyService(store, encryptionService, store)
8687
secretStorageService := server.NewSecretStorageService(store, accessKeyService)
8788
environmentService := server.NewEnvironmentService(store, encryptionService)
89+
subscriptionService := proServer.NewSubscriptionService()
8890

8991
taskPool := tasks.CreateTaskPool(
9092
store,
@@ -130,6 +132,7 @@ func runService() {
130132
secretStorageService,
131133
accessKeyService,
132134
environmentService,
135+
subscriptionService,
133136
)
134137

135138
route.Use(func(next http.Handler) http.Handler {

pkg/random/string.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,27 @@ import (
66
)
77

88
const (
9-
chars = "abcdefghijklmnopqrstuvwxyz0123456789"
9+
digits = "0123456789"
10+
chars = "abcdefghijklmnopqrstuvwxyz0123456789"
1011
)
1112

12-
func String(strlen int) string {
13+
func rnd(strlen int, baseStr string) string {
1314
result := make([]byte, strlen)
14-
charLen := big.NewInt(int64(len(chars)))
15+
charLen := big.NewInt(int64(len(baseStr)))
1516
for i := range result {
1617
r, err := rand.Int(rand.Reader, charLen)
1718
if err != nil {
1819
panic(err)
1920
}
20-
result[i] = chars[r.Int64()]
21+
result[i] = baseStr[r.Int64()]
2122
}
2223
return string(result)
2324
}
25+
26+
func Number(strlen int) string {
27+
return rnd(strlen, digits)
28+
}
29+
30+
func String(strlen int) string {
31+
return rnd(strlen, chars)
32+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package server
2+
3+
import (
4+
"github.com/semaphoreui/semaphore/services/interfaces"
5+
)
6+
7+
func NewSubscriptionService() interfaces.SubscriptionService {
8+
return nil
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package interfaces
2+
3+
import "github.com/semaphoreui/semaphore/db"
4+
5+
type SubscriptionService interface {
6+
HasActiveSubscription() bool
7+
CanAddProUser(store db.Store) (ok bool, err error)
8+
}

0 commit comments

Comments
 (0)