Add API to get a single property. related PrivateCaptcha/issues#45

This commit is contained in:
Taras Kushnir
2025-12-18 18:17:51 +01:00
parent 6e6174bedf
commit f446e2884d
4 changed files with 181 additions and 0 deletions

View File

@@ -742,3 +742,47 @@ func (s *Server) getOrgProperties(w http.ResponseWriter, r *http.Request) {
}
s.sendAPISuccessResponseEx(ctx, response, w, cacheHeaders)
}
func (s *Server) getOrgProperty(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user, _, err := s.requestUser(ctx)
if err != nil {
s.sendHTTPErrorResponse(err, w)
return
}
org, err := s.requestOrg(user, r, false /*only owner*/)
if err != nil {
if err == db.ErrInvalidInput {
s.sendAPIErrorResponse(ctx, common.StatusOrgIDInvalidError, r, w)
} else {
s.sendHTTPErrorResponse(err, w)
}
return
}
property, err := s.requestProperty(org, r)
if err != nil {
if err == db.ErrSoftDeleted {
s.sendAPIErrorResponse(ctx, common.StatusPropertyIDInvalidError, r, w)
} else {
s.sendHTTPErrorResponse(err, w)
}
return
}
data := &apiPropertyOutput{
ID: s.IDHasher.Encrypt(int(property.ID)),
Name: property.Name,
Domain: property.Domain,
Sitekey: db.UUIDToSiteKey(property.ExternalID),
Level: int(property.Level.Int16),
Growth: string(property.Growth),
ValiditySeconds: int(property.ValidityInterval.Seconds()),
AllowSubdomains: property.AllowSubdomains,
AllowLocalhost: property.AllowLocalhost,
MaxReplayCount: int(property.MaxReplayCount),
}
s.sendAPISuccessResponse(ctx, data, w)
}

View File

@@ -634,3 +634,109 @@ func TestApiGetProperties(t *testing.T) {
t.Fatalf("Unexpected number of properties: %v", actual)
}
}
func TestApiGetProperty(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := common.TraceContext(t.Context(), t.Name())
user, org, apiKey, err := setupAPISuite(ctx, t.Name())
if err != nil {
t.Fatal(err)
}
property, _, err := s.BusinessDB.Impl().CreateNewProperty(ctx, db_test.CreateNewPropertyParams(user.ID, "example.com"), org)
if err != nil {
t.Fatal(err)
}
propertyID := s.IDHasher.Encrypt(int(property.ID))
output, meta, err := requestResponseAPISuite[*apiPropertyOutput](ctx, nil,
http.MethodGet,
fmt.Sprintf("/%s/%s/%s/%s", common.OrgEndpoint, s.IDHasher.Encrypt(int(org.ID)),
common.PropertyEndpoint, propertyID),
apiKey)
if err != nil {
t.Fatal(err)
}
if !meta.Code.Success() {
t.Fatalf("Unexpected status code: %v", meta.Description)
}
if output.ID != propertyID {
t.Errorf("Received property ID %v but %v expected", output.ID, property.ID)
}
if output.Name != property.Name {
t.Errorf("Received property Name %v, but %v expected", output.Name, property.Name)
}
if output.Sitekey != db.UUIDToSiteKey(property.ExternalID) {
t.Errorf("Unexpected property sitekey: %v", output.Sitekey)
}
if output.Level != int(property.Level.Int16) {
t.Errorf("Received property Level %v but %v expected", output.Level, property.Level.Int16)
}
if output.Growth != string(property.Growth) {
t.Errorf("Received property Growth %v but %v expected", output.Growth, property.Growth)
}
if output.ValiditySeconds != int(property.ValidityInterval.Seconds()) {
t.Errorf("Received property Validity Seconds %v but %v expected", output.ValiditySeconds, property.ValidityInterval.Seconds())
}
if output.AllowSubdomains != property.AllowSubdomains {
t.Errorf("Received property Subdomains %v but %v expected", output.AllowSubdomains, property.AllowSubdomains)
}
if output.AllowLocalhost != property.AllowLocalhost {
t.Errorf("Received property Localhost %v but %v expected", output.AllowLocalhost, property.AllowLocalhost)
}
if output.MaxReplayCount != int(property.MaxReplayCount) {
t.Errorf("Received property MaxReplayCount %v but %v expected", output.MaxReplayCount, property.MaxReplayCount)
}
}
func TestApiGetPropertyPermissions(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := common.TraceContext(t.Context(), t.Name())
owner, org, _, err := setupAPISuite(ctx, t.Name())
if err != nil {
t.Fatal(err)
}
property, _, err := s.BusinessDB.Impl().CreateNewProperty(ctx, db_test.CreateNewPropertyParams(owner.ID, "example.com"), org)
if err != nil {
t.Fatal(err)
}
_, _, apiKey, err := setupAPISuite(ctx, t.Name()+"_user2")
if err != nil {
t.Fatal(err)
}
propertyID := s.IDHasher.Encrypt(int(property.ID))
resp, err := apiRequestSuite(ctx, nil,
http.MethodGet,
fmt.Sprintf("/%s/%s/%s/%s", common.OrgEndpoint, s.IDHasher.Encrypt(int(org.ID)),
common.PropertyEndpoint, propertyID),
apiKey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusForbidden {
t.Fatalf("Unexpected status code: %v", resp.StatusCode)
}
}

View File

@@ -69,3 +69,16 @@ type apiAsyncTaskResultOutput struct {
Finished bool `json:"finished"`
Result interface{} `json:"result"`
}
type apiPropertyOutput struct {
ID string `json:"id"`
Name string `json:"name"`
Domain string `json:"domain"`
Sitekey string `json:"sitekey"`
Level int `json:"level,omitempty"`
Growth string `json:"growth,omitempty"`
ValiditySeconds int `json:"validity_seconds,omitempty"`
AllowSubdomains bool `json:"allow_subdomains,omitempty"`
AllowLocalhost bool `json:"allow_localhost,omitempty"`
MaxReplayCount int `json:"max_replay_count,omitempty"`
}

View File

@@ -42,6 +42,7 @@ func (s *Server) setupEnterprise(rg *common.RouteGenerator, publicChain alice.Ch
rg.Handle(rg.Post(common.OrgEndpoint, arg(common.ParamOrg), common.PropertiesEndpoint), portalAPIChain, http.MaxBytesHandler(http.HandlerFunc(s.postNewProperties), maxPostPropertiesBodySize))
rg.Handle(rg.Delete(common.PropertiesEndpoint), portalAPIChain, http.MaxBytesHandler(http.HandlerFunc(s.deleteProperties), maxDeletePropertiesBodySize))
rg.Handle(rg.Patch(common.PropertiesEndpoint), portalAPIChain, http.MaxBytesHandler(http.HandlerFunc(s.updateProperties), maxUpdatePropertiesBodySize))
rg.Handle(rg.Get(common.OrgEndpoint, arg(common.ParamOrg), common.PropertyEndpoint, arg(common.ParamProperty)), portalAPIChain, http.HandlerFunc(s.getOrgProperty))
}
func (s *Server) RegisterTaskHandlers(ctx context.Context) {
@@ -98,6 +99,23 @@ func (s *Server) requestOrg(user *dbgen.User, r *http.Request, onlyOwner bool) (
return org, nil
}
func (s *Server) requestProperty(org *dbgen.Organization, r *http.Request) (*dbgen.Property, error) {
ctx := r.Context()
propertyID, value, err := common.IntPathArg(r, common.ParamProperty, s.IDHasher)
if err != nil {
slog.ErrorContext(ctx, "Failed to parse property path parameter", "value", value, common.ErrAttr(err))
return nil, db.ErrInvalidInput
}
property, err := s.BusinessDB.Impl().RetrieveOrgProperty(ctx, org, propertyID)
if err != nil {
return nil, err
}
return property, nil
}
func (s *Server) sendHTTPErrorResponse(err error, w http.ResponseWriter) {
switch err {
case db.ErrRecordNotFound: