Skip to content

Commit 4fc72ac

Browse files
authored
Is677/trial account (ITISFoundation#3352)
1 parent ecb8b35 commit 4fc72ac

File tree

31 files changed

+1032
-213
lines changed

31 files changed

+1032
-213
lines changed

api/specs/webserver/components/schemas/me.yaml

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,17 @@ ProfileCommon:
99
first_name: Pedro
1010
last_name: Crespo
1111

12-
ProfileInput:
12+
ProfileUpdate:
1313
allOf:
1414
- $ref: "#/ProfileCommon"
15-
example:
16-
first_name: Pedro
17-
last_name: Crespo
1815

19-
ProfileOutput:
16+
ProfileGet:
2017
allOf:
2118
- $ref: "#/ProfileCommon"
2219
- type: object
2320
properties:
21+
id:
22+
type: integer
2423
login:
2524
type: string
2625
format: email
@@ -30,18 +29,18 @@ ProfileOutput:
3029
$ref: "./group.yaml#/AllUsersGroups"
3130
gravatar_id:
3231
type: string
33-
example:
34-
35-
role: Admin
36-
gravatar_id: 205e460b479e2e5b48aec07710c08d50
32+
expirationDate:
33+
type: string
34+
format: date
35+
description: "If user has a trial account, it sets the expiration date, otherwise None"
3736

3837
ProfileEnveloped:
3938
type: object
4039
required:
4140
- data
4241
properties:
4342
data:
44-
$ref: "#/ProfileOutput"
43+
$ref: "#/ProfileGet"
4544
error:
4645
nullable: true
4746
default: null
@@ -63,9 +62,6 @@ Token:
6362
required:
6463
- service
6564
- token_key
66-
example:
67-
service: "github-api-v1"
68-
token_key: N1BP5ZSpB
6965

7066
TokenId:
7167
description: toke identifier

api/specs/webserver/openapi-user.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ paths:
2121
content:
2222
application/json:
2323
schema:
24-
$ref: "./components/schemas/me.yaml#/ProfileInput"
24+
$ref: "./components/schemas/me.yaml#/ProfileUpdate"
2525
responses:
2626
"204":
2727
description: updated profile

api/specs/webserver/openapi.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
openapi: 3.0.0
22
info:
33
title: "osparc-simcore web API"
4-
version: 0.7.0
4+
version: 0.8.0
55
description: "API designed for the front-end app"
66
contact:
77
name: IT'IS Foundation

packages/models-library/src/models_library/basic_types.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from enum import Enum
22

3-
from pydantic import conint, constr
3+
from pydantic import PositiveInt, conint, constr
44

55
from .basic_regex import UUID_RE, VERSION_RE
66

@@ -23,6 +23,9 @@
2323
# e.g. '5c833a78-1af3-43a7-9ed7-6a63b188f4d8'
2424
UUIDStr = constr(regex=UUID_RE)
2525

26+
# auto-incremented primary-key IDs
27+
IdInt = PrimaryKeyInt = PositiveInt
28+
2629

2730
class LogLevel(str, Enum):
2831
DEBUG = "DEBUG"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""User expiration
2+
3+
Revision ID: ead667546b12
4+
Revises: 9d477e20d06e
5+
Create Date: 2022-09-12 14:09:04.385524+00:00
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
11+
# revision identifiers, used by Alembic.
12+
revision = "ead667546b12"
13+
down_revision = "9d477e20d06e"
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade():
19+
op.add_column("users", sa.Column("expires_at", sa.DateTime(), nullable=True))
20+
21+
# https://medium.com/makimo-tech-blog/upgrading-postgresqls-enum-type-with-sqlalchemy-using-alembic-migration-881af1e30abe
22+
with op.get_context().autocommit_block():
23+
op.execute("ALTER TYPE userstatus ADD VALUE 'EXPIRED'")
24+
25+
26+
def downgrade():
27+
op.drop_column("users", "expires_at")
28+
29+
# https://medium.com/makimo-tech-blog/upgrading-postgresqls-enum-type-with-sqlalchemy-using-alembic-migration-881af1e30abe
30+
op.execute("ALTER TYPE userstatus RENAME TO userstatus_old")
31+
op.execute(
32+
"CREATE TYPE userstatus AS ENUM('CONFIRMATION_PENDING', 'ACTIVE', 'BANNED')"
33+
)
34+
op.execute(
35+
"ALTER TABLE users ALTER COLUMN status TYPE userstatus USING "
36+
"status::text::userstatus"
37+
)
38+
op.execute("DROP TYPE userstatus_old")

packages/postgres-database/src/simcore_postgres_database/models/users.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,37 @@ class UserStatus(Enum):
5959
"""
6060
pending: user registered but not confirmed
6161
active: user is confirmed and can use the platform
62+
expired: user is not authorized because it expired after a trial period
6263
banned: user is not authorized
6364
"""
6465

6566
CONFIRMATION_PENDING = "PENDING"
6667
ACTIVE = "ACTIVE"
68+
EXPIRED = "EXPIRED"
6769
BANNED = "BANNED"
6870

6971

7072
users = sa.Table(
7173
"users",
7274
metadata,
73-
sa.Column("id", sa.BigInteger, nullable=False),
74-
sa.Column("name", sa.String, nullable=False),
75-
sa.Column("email", sa.String, nullable=False),
75+
sa.Column(
76+
"id",
77+
sa.BigInteger,
78+
nullable=False,
79+
doc="Primary key for user identifier",
80+
),
81+
sa.Column(
82+
"name",
83+
sa.String,
84+
nullable=False,
85+
doc="Display name. NOTE: this is NOT a user name since uniqueness is NOT guaranteed",
86+
),
87+
sa.Column(
88+
"email",
89+
sa.String,
90+
nullable=False,
91+
doc="User email is used as username since it is a unique human-readable identifier",
92+
),
7693
sa.Column(
7794
"phone",
7895
sa.String,
@@ -89,30 +106,57 @@ class UserStatus(Enum):
89106
onupdate="CASCADE",
90107
ondelete="RESTRICT",
91108
),
109+
doc="User's group ID",
92110
),
93111
sa.Column(
94112
"status",
95113
sa.Enum(UserStatus),
96114
nullable=False,
97115
default=UserStatus.CONFIRMATION_PENDING,
116+
doc="Status of the user account. SEE UserStatus",
117+
),
118+
sa.Column(
119+
"role",
120+
sa.Enum(UserRole),
121+
nullable=False,
122+
default=UserRole.USER,
123+
doc="Use for role-base authorization",
124+
),
125+
sa.Column(
126+
"created_at",
127+
sa.DateTime(),
128+
nullable=False,
129+
server_default=func.now(),
130+
doc="Registration timestamp",
98131
),
99-
sa.Column("role", sa.Enum(UserRole), nullable=False, default=UserRole.USER),
100-
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=func.now()),
101132
sa.Column(
102133
"modified",
103134
sa.DateTime(),
104135
nullable=False,
105136
server_default=func.now(),
106137
onupdate=func.now(), # this will auto-update on modification
138+
doc="Last modification timestamp",
139+
),
140+
sa.Column(
141+
"expires_at",
142+
sa.DateTime(),
143+
nullable=True,
144+
doc="Sets the expiration date for trial accounts."
145+
"If set to NULL then the account does not expire.",
146+
),
147+
sa.Column(
148+
"created_ip",
149+
sa.String(),
150+
nullable=True,
151+
doc="User IP from which use was created",
107152
),
108-
sa.Column("created_ip", sa.String(), nullable=True),
109-
#
153+
# ---------------------------
110154
sa.PrimaryKeyConstraint("id", name="user_pkey"),
111155
sa.UniqueConstraint("email", name="user_login_key"),
112156
sa.UniqueConstraint(
113157
"phone",
114158
name="user_phone_unique_constraint",
115-
# cannot use same phone for two users
159+
# NOTE: that cannot use same phone for two user accounts
116160
),
117161
)
118162

packages/postgres-database/tests/test_users.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
1-
from simcore_postgres_database.models.users import _USER_ROLE_TO_LEVEL, UserRole
1+
from datetime import datetime, timedelta
2+
from typing import Optional
3+
4+
import sqlalchemy as sa
5+
from aiopg.sa.engine import Engine
6+
from aiopg.sa.result import ResultProxy, RowProxy
7+
from pytest_simcore.helpers.rawdata_fakers import random_user
8+
from simcore_postgres_database.models.users import (
9+
_USER_ROLE_TO_LEVEL,
10+
UserRole,
11+
UserStatus,
12+
users,
13+
)
14+
from sqlalchemy.sql import func
215

316

417
def test_user_role_to_level_map_in_sync():
@@ -48,3 +61,45 @@ def test_user_role_comparison():
4861

4962
assert UserRole.ADMIN <= UserRole.ADMIN
5063
assert UserRole.ADMIN == UserRole.ADMIN
64+
65+
66+
async def test_trial_accounts(pg_engine: Engine):
67+
EXPIRATION_INTERVAL = timedelta(minutes=5)
68+
69+
async with pg_engine.acquire() as conn:
70+
71+
# creates trial user
72+
client_now = datetime.utcnow()
73+
user_id: Optional[int] = await conn.scalar(
74+
users.insert()
75+
.values(
76+
**random_user(
77+
status=UserStatus.ACTIVE,
78+
# Using some magic from sqlachemy ...
79+
expires_at=func.now() + EXPIRATION_INTERVAL,
80+
)
81+
)
82+
.returning(users.c.id)
83+
)
84+
assert user_id
85+
86+
# check expiration date
87+
result: ResultProxy = await conn.execute(
88+
sa.select([users.c.status, users.c.created_at, users.c.expires_at]).where(
89+
users.c.id == user_id
90+
)
91+
)
92+
row: Optional[RowProxy] = await result.first()
93+
assert row
94+
assert row.created_at - client_now < timedelta(
95+
minutes=1
96+
), "Difference between server and client now should not differ much"
97+
assert row.expires_at - row.created_at == EXPIRATION_INTERVAL
98+
assert row.status == UserStatus.ACTIVE
99+
100+
# sets user as expired
101+
await conn.execute(
102+
users.update()
103+
.values(status=UserStatus.EXPIRED)
104+
.where(users.c.id == user_id)
105+
)

services/web/server/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.7.0
1+
0.8.0

services/web/server/setup.cfg

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[bumpversion]
2-
current_version = 0.7.0
2+
current_version = 0.8.0
33
commit = True
44
message = services/webserver api version: {current_version} → {new_version}
55
tag = False
6-
commit-args = --no-verify
6+
commit_args = --no-verify
77

88
[bumpversion:file:VERSION]
99

@@ -14,6 +14,6 @@ commit-args = --no-verify
1414
[tool:pytest]
1515
addopts = --strict-markers
1616
asyncio_mode = auto
17-
markers =
17+
markers =
1818
slow: marks tests as slow (deselect with '-m "not slow"')
1919
acceptance_test: "marks tests as 'acceptance tests' i.e. does the system do what the user expects? Typically those are workflows."

services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
openapi: 3.0.0
22
info:
33
title: osparc-simcore web API
4-
version: 0.7.0
4+
version: 0.8.0
55
description: API designed for the front-end app
66
contact:
77
name: IT'IS Foundation
@@ -664,6 +664,8 @@ paths:
664664
- $ref: '#/paths/~1me/put/requestBody/content/application~1json/schema/allOf/0'
665665
- type: object
666666
properties:
667+
id:
668+
type: integer
667669
login:
668670
type: string
669671
format: email
@@ -673,10 +675,10 @@ paths:
673675
$ref: '#/paths/~1groups/get/responses/200/content/application~1json/schema/properties/data'
674676
gravatar_id:
675677
type: string
676-
example:
677-
678-
role: Admin
679-
gravatar_id: 205e460b479e2e5b48aec07710c08d50
678+
expirationDate:
679+
type: string
680+
format: date
681+
description: 'If user has a trial account, it sets the expiration date, otherwise None'
680682
error:
681683
nullable: true
682684
default: null
@@ -700,9 +702,6 @@ paths:
700702
example:
701703
first_name: Pedro
702704
last_name: Crespo
703-
example:
704-
first_name: Pedro
705-
last_name: Crespo
706705
responses:
707706
'204':
708707
description: updated profile
@@ -763,9 +762,6 @@ paths:
763762
required:
764763
- service
765764
- token_key
766-
example:
767-
service: github-api-v1
768-
token_key: N1BP5ZSpB
769765
error:
770766
nullable: true
771767
default: null
@@ -2470,12 +2466,12 @@ paths:
24702466
CreationStatus:
24712467
operationId: get_task_status
24722468
parameters:
2473-
task_id: '$response.body#/data/task_id'
2469+
task_id: $response.body#/data/task_id
24742470
CreationResult:
24752471
operationId: get_task_result
24762472
description: Returns 201 if creation succeeded
24772473
parameters:
2478-
task_id: '$response.body#/data/task_id'
2474+
task_id: $response.body#/data/task_id
24792475
default:
24802476
$ref: '#/components/responses/DefaultErrorResponse'
24812477
/projects/active:

0 commit comments

Comments
 (0)