11from __future__ import annotations
22
33import datetime
4+ from typing import Iterator
45
56import sqlalchemy .orm
6- from sqlalchemy .orm import Mapped , mapped_column , relationship
7- from sqlalchemy import String , Integer , ForeignKey , DateTime
7+ from sqlalchemy import String , Integer , ForeignKey , DateTime , Boolean
88from sqlalchemy .ext .hybrid import hybrid_property
9+ from sqlalchemy .orm import Mapped , mapped_column , relationship , backref
910
10- from auth_backend .models .base import Base
11+ from auth_backend .models .base import BaseDbModel
1112
1213
1314class ParamDict :
14-
1515 # Type hints
1616 email : AuthMethod
1717 hashed_password : AuthMethod
@@ -32,12 +32,28 @@ def __new__(cls, methods: list[AuthMethod], *args, **kwargs):
3232 return obj
3333
3434
35- class User (Base ):
36-
37- id : Mapped [int ] = mapped_column (Integer , primary_key = True )
35+ class User (BaseDbModel ):
36+ is_deleted : Mapped [bool ] = mapped_column (Boolean , default = False )
37+
38+ _auth_methods : Mapped [list [AuthMethod ]] = relationship (
39+ "AuthMethod" ,
40+ foreign_keys = "AuthMethod.user_id" ,
41+ primaryjoin = "and_(User.id==AuthMethod.user_id, not_(AuthMethod.is_deleted))" ,
42+ )
43+ sessions : Mapped [list [UserSession ]] = relationship (
44+ "UserSession" , foreign_keys = "UserSession.user_id" , back_populates = "user"
45+ )
46+ groups : Mapped [list [Group ]] = relationship (
47+ "Group" ,
48+ secondary = "user_group" ,
49+ back_populates = "users" ,
50+ primaryjoin = "and_(User.id==UserGroup.user_id, not_(UserGroup.is_deleted))" ,
51+ secondaryjoin = "and_(Group.id==UserGroup.group_id, not_(Group.is_deleted))" ,
52+ )
3853
39- _auth_methods : Mapped [list ["AuthMethod" ]] = relationship ("AuthMethod" , foreign_keys = "AuthMethod.user_id" )
40- sessions : Mapped [list ["UserSession" ]] = relationship ("UserSession" , foreign_keys = "UserSession.user_id" )
54+ @hybrid_property
55+ def active_sessions (self ) -> list :
56+ return [row for row in self .sessions if not row .expired ]
4157
4258 @hybrid_property
4359 def auth_methods (self ) -> ParamDict :
@@ -49,24 +65,68 @@ def auth_methods(self) -> ParamDict:
4965 return ParamDict .__new__ (ParamDict , self ._auth_methods )
5066
5167
52- class AuthMethod ( Base ):
68+ class Group ( BaseDbModel ):
5369 id : Mapped [int ] = mapped_column (Integer , primary_key = True )
70+ name : Mapped [str ] = mapped_column (String , unique = True , nullable = False )
71+ parent_id : Mapped [int ] = mapped_column (Integer , ForeignKey ("group.id" ), nullable = True )
72+ create_ts : Mapped [datetime .datetime ] = mapped_column (DateTime , default = datetime .datetime .utcnow )
73+ is_deleted : Mapped [bool ] = mapped_column (Boolean , default = False )
74+
75+ child : Mapped [list [Group ]] = relationship (
76+ "Group" ,
77+ backref = backref ("parent" , remote_side = [id ]),
78+ primaryjoin = "and_(Group.id==Group.parent_id, not_(Group.is_deleted))" ,
79+ )
80+ users : Mapped [list [User ]] = relationship (
81+ "User" ,
82+ secondary = "user_group" ,
83+ back_populates = "groups" ,
84+ primaryjoin = "and_(Group.id==UserGroup.group_id, not_(UserGroup.is_deleted))" ,
85+ secondaryjoin = "and_(User.id==UserGroup.user_id, not_(User.is_deleted))" ,
86+ )
87+
88+ @hybrid_property
89+ def parents (self ) -> Iterator [Group ]:
90+ parent = self
91+ while parent := parent .parent :
92+ yield parent
93+
94+
95+ class UserGroup (BaseDbModel ):
96+ user_id : Mapped [int ] = mapped_column (Integer , ForeignKey ("user.id" ))
97+ group_id : Mapped [int ] = mapped_column (Integer , ForeignKey ("group.id" ))
98+ is_deleted : Mapped [bool ] = mapped_column (Boolean , default = False )
99+
100+
101+ class AuthMethod (BaseDbModel ):
54102 user_id : Mapped [int ] = mapped_column (Integer , ForeignKey ("user.id" ))
55103 auth_method : Mapped [str ] = mapped_column (String )
56104 param : Mapped [str ] = mapped_column (String )
57105 value : Mapped [str ] = mapped_column (String )
106+ is_deleted : Mapped [bool ] = mapped_column (Boolean , default = False )
58107
59- user : Mapped ["User" ] = relationship ("User" , foreign_keys = [user_id ], back_populates = "_auth_methods" )
108+ user : Mapped [User ] = relationship (
109+ "User" ,
110+ foreign_keys = [user_id ],
111+ back_populates = "_auth_methods" ,
112+ primaryjoin = "and_(AuthMethod.user_id==User.id, not_(User.is_deleted))" ,
113+ )
60114
61115
62- class UserSession (Base ):
63- id : Mapped [int ] = mapped_column (Integer , primary_key = True )
116+ class UserSession (BaseDbModel ):
64117 user_id : Mapped [int ] = mapped_column (Integer , sqlalchemy .ForeignKey ("user.id" ))
65- expires : Mapped [datetime .datetime ] = mapped_column (DateTime , default = datetime .datetime .utcnow () + datetime .timedelta (days = 7 ))
118+ expires : Mapped [datetime .datetime ] = mapped_column (
119+ DateTime , default = datetime .datetime .utcnow () + datetime .timedelta (days = 7 )
120+ )
66121 token : Mapped [str ] = mapped_column (String , unique = True )
67122
68- user : Mapped ["User" ] = relationship ("User" , foreign_keys = [user_id ], back_populates = "sessions" )
123+ user : Mapped [User ] = relationship (
124+ "User" ,
125+ foreign_keys = [user_id ],
126+ back_populates = "sessions" ,
127+ primaryjoin = "and_(UserSession.user_id==User.id, not_(User.is_deleted))" ,
128+ )
69129
70130 @hybrid_property
71- def expired (self ):
131+ def expired (self ) -> bool :
72132 return self .expires <= datetime .datetime .utcnow ()
0 commit comments