Is this basic code for user login and management rational? How do I use their WebSocket from their GenServer?

I am trying to set up a simple way to:

  • Log a user in (auth them against separate database server),
  • Manage their activity while logged in (likely by one GenServer per user), and
  • Keep track of all logged in users (add to global registry ie. Syn).

I have never built a server or done this before. I have read a few Elixir books and many websites, but that is all. So I don’t know if anything is rational.

I am wondering if I have the right idea and if so, (1) how can I upgrade their websocket and send JWT back at same time, and (2) how I can set use their WebSocket from their GenServer (set a reference for it into GenServer state?).

Starting with the EchoServer type websocket design as in the Plug documentation here, my idea conceptually is to do it like this:

AUTHENTICATION

1) User makes HTTP request to login

  • User sends HTTPS request to server to login with embedded username/password or valid JWT for login in header.
  • This is received in use Plug.Router module from demo Plug code above under get "/login" hypothetically.

(i) Username/password:

  • If user/password received, inside this function, send a HTTP request (or other communication) to database server to verify it.
  • If database server replies okay, we create a JWT auth token and respond from this get "/login" to the user with it so they have a copy.
  • We also upgrade their connection all at once (if we can do both at once).

(ii) JWT:

  • If JWT received, verify it inside this get "/login" function.
  • If valid, upgrade to websocket.

NEXT STEPS?
Now we have a connected verified websocket user. Now we need to:

  1. Start a per-user GenServer to track their state and set into it their token.
  2. Register the username and this GenServer as “logged in” in global registry (eg. Syn).
  3. Put into their GenServer some reference also for the websocket connection (EchoServer, conn, whatever?) so the GenServer can send or respond to the user messages through it.

So I think this could look something like this:

get "/login" do

	# check headers for if there is a username/pass supplied 
	is_user_pass_bool = is_user_pass(conn.req_headers)

	# username/pass found
	token = if (is_user_pass_bool) do 

		# send HTTP Request or other communication to database server to validate user/pass
		db_response = HTTPoison.post(db_url, db_msg_body, db_msg_headers) # whatever
 		valid_login = check_db_response(db_response)
		
		# valid login, make a token 
		if (valid_login) do
			make_token(conn.req_headers) # make the token
		else
			nil # no token to give, invalid login attempt, or db server broken
		end
	else
		# try to pull a token out of header
		extract_token_from_header(conn.req_headers)
	end

	# check if token is valid
	is_valid_token_bool = validate(token)

	# manage valid login attempt
	if (is_valid_token_bool) do

		# send back token copy and upgrade connection (can we do all at once?)
		conn
		|> put_resp_header([{"token", token}]) # put JWT token into header response in some manner
		|> put_resp_content_type("text/plain")
		|> send_resp(200, "okay, starting websocket with this token")

		# (WILL THIS WORK? OR HAVE I LOST THE CONNECTION NOW BY SEND_RESP?)
		conn
		|> WebSockAdapter.upgrade(EchoServer, [], timeout: 60_000)
		|> halt()

		# create GenServer to manage user
		socket_reference = ??? # how do i keep a reference to their socket to continue sending things to them in their GenServer?
		user_gen_server = {UserGenServer, {token, socket_reference}} 
		start_result = Supervisor.start_link([user_gen_server], [strategy: :one_for_one, name: MainServer.Supervisor]) 

		# check result and register user if succeeded
		case (start_result) do
			{:ok, gen_server_pid} ->

				# add user to Syn database with user name and pid of their gen server 
				user_name = get_user_name(token)
				:syn.register(:user_registry, user_name, gen_server_pid)  # maybe also check if already registered...
			_ ->
				# something went wrong starting UserGenServer, try again or figure out problem
		end
	
	end

	# manage bad login attempts (more detail can be added)
	if (!valid_token_bool && !is_user_pass_bool) do

		# bad or expired token, cleint must discard token and try another user/pass
	        conn
        	|> put_resp_content_type("text/plain")
	        |> send_resp(200, "bad token, require new username/pass attempt")
	end
	if (!valid_token_bool && is_user_pass_bool) do

		# bad user/pass or server down, let them know
	        conn
        	|> put_resp_content_type("text/plain")
	        |> send_resp(200, "bad user name or server is screwed up, try again")
	end

end

What do you think? Is this all remotely rational or how people would do this? I have never built a server before so I don’t know.

Two main points come to mind for me:

(1) Can I reply to their valid login attempt with their token (so they have a copy of it) and also upgrade them to a websocket at the same time as I did? Or does sending the response with the token break the connection? How does this work?

(2) How do I reference or use their websocket from inside the UserGenServer I created? Ie. How do I run a function from the UserGenServer that sends the client a WebSocket message saying “Hello”? Or how do I based on a given WebSocket message the client sends me like “Get new data” trigger their GenServer to fun a certain function to accomplish this and reply?

Thanks for any further help or ideas.