@@ -106,99 +106,66 @@ defmodule EpochtalkServer.Session do
106106 end
107107
108108 @ doc """
109- Gets all sessions for a specific `User`
109+ Gets all session ids for a specific user_id
110110 """
111- @ spec get_sessions ( user :: User . t ( ) ) ::
112- { :ok , user :: User . t ( ) , sessions :: [ String . t ( ) ] }
111+ @ spec get_sessions ( user_id :: non_neg_integer ) ::
112+ { :ok , sessions_ids :: [ String . t ( ) ] }
113113 | { :error , atom ( ) | Redix.Error . t ( ) | Redix.ConnectionError . t ( ) }
114- def get_sessions ( % User { } = user ) do
114+ def get_sessions ( user_id ) do
115115 # get session id's from redis under "user:{user_id}:sessions"
116- session_key = generate_key ( user . id , "sessions" )
117-
118- case Redix . command ( :redix , [ "SMEMBERS" , session_key ] ) do
119- { :ok , sessions } ->
120- session_ids =
121- sessions
122- |> Enum . map ( fn session ->
123- [ session_id , _expiration ] = session |> String . split ( ":" )
124- session_id
125- end )
126-
127- { :ok , user , session_ids }
116+ session_key = generate_key ( user_id , "sessions" )
128117
129- { :error , error } ->
130- { :error , error }
131- end
118+ Redix . command ( :redix , [ "ZRANGE" , session_key , 0 , - 1 ] )
132119 end
133120
134121 defp is_active_for_user_id ( session_id , user_id ) do
135- case get_session_ids_by_user_id ( user_id ) do
136- { :error , error } ->
137- { :error , error }
138-
139- session_ids ->
140- Enum . member? ( session_ids , session_id )
141- end
142- end
143-
144- defp get_sessions_by_user_id ( user_id ) do
145- # get session id's from redis under "user:{user_id}:sessions"
146122 session_key = generate_key ( user_id , "sessions" )
147-
148- case Redix . command ( :redix , [ "SMEMBERS" , session_key ] ) do
149- { :ok , sessions } -> sessions
150- { :error , error } -> { :error , error }
151- end
152- end
153-
154- defp get_session_ids_by_user_id ( user_id ) do
155- case get_sessions_by_user_id ( user_id ) do
123+ # check ZSCORE for user_id:session_id pair
124+ # returns score if it is a member of the set
125+ # returns nil if not a member
126+ case Redix . command ( :redix , [ "ZSCORE" , session_key , session_id ] ) do
156127 { :error , error } -> { :error , error }
157- sessions -> Enum . map ( sessions , & ( String . split ( & 1 , ":" ) |> List . first ( ) ) )
128+ { :ok , nil } -> false
129+ { :ok , _score } -> true
158130 end
159131 end
160132
161133 @ doc """
162- Deletes a specific `User` session
134+ Deletes the session specified in conn
163135 """
164- @ spec delete_session (
165- user :: User . t ( ) ,
166- session_id :: String . t ( )
167- ) ::
168- { :ok , user :: User . t ( ) } | { :error , atom ( ) | Redix.Error . t ( ) | Redix.ConnectionError . t ( ) }
169- def delete_session ( % User { } = user , session_id ) do
170- # delete session id from redis under "user:{user_id}:sessions"
171- session_key = generate_key ( user . id , "sessions" )
172-
173- case Redix . command ( :redix , [ "SREM" , session_key , session_id ] ) do
174- { :ok , _ } -> { :ok , user }
175- { :error , error } -> { :error , error }
176- end
177- end
136+ @ spec delete ( conn :: Plug.Conn . t ( ) ) ::
137+ { :ok , conn :: Plug.Conn . t ( ) }
138+ | { :error , atom ( ) | Redix.Error . t ( ) | Redix.ConnectionError . t ( ) }
139+ def delete ( conn ) do
140+ # get user from guardian resource
141+ % { id: user_id } = Guardian.Plug . current_resource ( conn )
142+ # get session_id from jti in jwt token
143+ % { claims: % { "jti" => session_id } } =
144+ Guardian.Plug . current_token ( conn )
145+ |> Guardian . peek ( )
178146
179- defp delete_session_by_user_id ( user_id , session ) do
180- # delete session from redis under "user:{user_id}:sessions"
147+ # delete session id from redis under "user:{user_id}:sessions"
181148 session_key = generate_key ( user_id , "sessions" )
182149
183- case Redix . command ( :redix , [ "SREM" , session_key , session ] ) do
184- { :ok , _ } -> { :ok , user_id }
185- { :error , error } -> { :error , error }
150+ case Redix . command ( :redix , [ "ZREM" , session_key , session_id ] ) do
151+ { :ok , _ } ->
152+ # sign user out
153+ conn = Guardian.Plug . sign_out ( conn )
154+ { :ok , conn }
155+
156+ { :error , error } ->
157+ { :error , error }
186158 end
187159 end
188160
189161 @ doc """
190162 Deletes every session instance for the specified `User`
191163 """
192- @ spec delete_sessions ( user :: User . t ( ) ) :: :ok
164+ @ spec delete_sessions ( user :: User . t ( ) ) :: { :ok , non_neg_integer } | Redix.ConnectionError . t ( )
193165 def delete_sessions ( % User { } = user ) do
194166 # delete session id from redis under "user:{user_id}:sessions"
195167 session_key = generate_key ( user . id , "sessions" )
196-
197- case Redix . command ( :redix , [ "SPOP" , session_key ] ) do
198- { :ok , nil } -> :ok
199- # repeat until redix returns nil
200- _ -> delete_sessions ( user )
201- end
168+ Redix . command ( :redix , [ "DEL" , session_key ] )
202169 end
203170
204171 defp save ( % User { } = user , session_id , ttl ) do
@@ -290,39 +257,39 @@ defmodule EpochtalkServer.Session do
290257 maybe_extend_ttl ( ban_key , ttl , old_ttl )
291258 end
292259
293- # these two rules ensure that sessions will eventually be deleted:
294- # clean expired sessions and add a new one
295- # ttl expiry for this key will delete all sessions in the set
260+ # session storage uses "pseudo-expiry" (via redis sorted-set score)
261+ # these rules ensure that session storage will not grow indefinitely:
262+ #
263+ # on every new session add,
264+ # clean (delete) expired sessions by expiry
265+ # add the new session, with expiry
266+ # ttl expiry for this key will delete all sessions in the set
296267 defp add_session ( user_id , session_id , ttl ) do
297268 # save session id to redis under "user:{user_id}:sessions"
298269 session_key = generate_key ( user_id , "sessions" )
299- # current unix time (default :seconds)
300- now = DateTime . utc_now ( ) |> DateTime . to_unix ( )
301- # intended unix expiration of this session
302- unix_expiration = now + ttl
303270 # delete expired sessions
304- get_sessions_by_user_id ( user_id )
305- |> Enum . each ( fn session ->
306- [ _session_id , expiration ] = String . split ( session , ":" )
307-
308- if String . to_integer ( expiration ) < now do
309- delete_session_by_user_id ( user_id , session )
310- end
311- end )
271+ delete_expired_sessions ( session_key )
272+ # intended unix expiration of this session
273+ unix_expiration = current_time ( ) + ttl
312274
313275 # add new session, noting unix expiration
314- result =
315- Redix . command ( :redix , [
316- "SADD" ,
317- session_key ,
318- session_id <> ":" <> Integer . to_string ( unix_expiration )
319- ] )
276+ result = Redix . command ( :redix , [ "ZADD" , session_key , unix_expiration , session_id ] )
320277
321278 # set ttl
322279 maybe_extend_ttl ( session_key , ttl )
323280 result
324281 end
325282
283+ # delete expired sessions by expiration (sorted set score)
284+ defp delete_expired_sessions ( session_key ) do
285+ Redix . command ( :redix , [ "ZREMRANGEBYSCORE" , session_key , "-inf" , current_time ( ) ] )
286+ end
287+
288+ # current unix time (default :seconds)
289+ defp current_time ( ) do
290+ DateTime . utc_now ( ) |> DateTime . to_unix ( )
291+ end
292+
326293 defp generate_key ( user_id , "user" ) , do: "user:#{ user_id } "
327294 defp generate_key ( user_id , type ) , do: "user:#{ user_id } :#{ type } "
328295
0 commit comments