Advertisement
Guest User

Untitled

a guest
Aug 25th, 2016
79
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.86 KB | None | 0 0
  1. ##User accounts with Sinatra (no BCrypt)
  2.  
  3. ### Create migration
  4.  
  5. A very basic schema for a user typically looks like this:
  6.  
  7. ```ruby
  8. def change
  9. create_table :users do |t|
  10. t.string :username
  11. t.string :email
  12. t.string :password
  13. t.timestamps
  14. end
  15. end
  16. ```
  17. Users can have plenty of other information about them or their account stored in their table, of course-- <code>full_name</code>, <code>hometown</code>, <code>is_private</code>. If ever stumped about what to include in the schema, think of apps you use and the information included in your profile.
  18.  
  19. ### Make validations on the model
  20.  
  21. ```ruby
  22. class User < ActiveRecord::Base
  23. validates :username, :presence => true,
  24. :uniqueness => true
  25. validates :email, :presence => true,
  26. :uniqueness => true
  27. :format => {:with => /\w+@\w+\.\w+/)
  28. validates :password, :presence => true
  29. end
  30. ```
  31. Validations mean a User object won't ever be saved unless they meet this criteria. The above code says that a User cannot exist in the database unless it has
  32.  
  33. * a username (which is unique)
  34. * an email address (which is unique and is formatted based on a RegEx (this is a very loose RegEx example that says "at least one word character (letter, number, underscore) followed by an "@", followed by at least one word character followed by a ".", followed by at least one word character"))
  35. * a password
  36.  
  37. ###Make the routes
  38.  
  39. Routes will depend on your UX design. Should login and sign up be on separate pages? If creating an account requires a lot of fields, each should probably have its own page:
  40.  
  41. ```ruby
  42. get '/login' do
  43. erb :login
  44. end
  45.  
  46. get '/new_account' do
  47. erb :new_account
  48. end
  49. ```
  50.  
  51. If creating an account is not much different from logging in, however, it may make more sense to consolidate both forms on the same page.
  52.  
  53. ```ruby
  54. get '/login' do
  55. erb :login
  56. end
  57. ```
  58.  
  59. Regardless, each form will need to POST to its own route.
  60.  
  61. ```ruby
  62. post '/login' do
  63. # params are passed in from the form
  64. # if user is authenticated,
  65. # "logs user in"
  66. # (which really just means setting the session cookie)
  67. # redirects the user somewhere (maybe '/' ?)
  68. # else if user auth fails,
  69. # redirects user, probably back to '/login'
  70. end
  71.  
  72. post '/new_account' do
  73. # (params are passed in from the form)
  74. user = User.create(params[:user])
  75. session[:user_id] = user.id
  76. # redirects user somewhere (maybe '/' ?)
  77. end
  78. ```
  79. And of course you'll need:
  80.  
  81. ```ruby
  82. get '/logout' do
  83. session[:user_id] = nil
  84. redirect_to '/'
  85. end
  86. ```
  87. Might your users also have a profile page? Maybe something like:
  88.  
  89. ```ruby
  90. get '/users/:username' do
  91. erb :user
  92. end
  93. ```
  94.  
  95. ### Make the current_user helper method
  96.  
  97. The Sinatra skeleton contains a "helpers" folder under "app". Methods saved in this folder are accessible from all your controllers and views.
  98.  
  99. Create a file called <code>user_helper.rb</code> (or something akin to that), and create a method <code>current_user</code>:
  100.  
  101. ```ruby
  102. def current_user
  103. User.find(session[:user_id]) if session[:user_id]
  104. end
  105. ```
  106. If a user has signed in and <code>session[:user_id]</code> has been set, calling this method will return the relevant User object. Else, the method will return <code>nil</code>.
  107.  
  108. There are a few other ways to write code that produces the same results. This one is the simplest.
  109.  
  110. ### Link to the views
  111.  
  112. Users should *probably* be able to sign in or logout or access their profile page at any time, regardless of which page they're on, right? (Probably right.) Put those babies in a header above your <code><%= yield %></code> statement in your <code>layout.erb</code> view.
  113.  
  114. ```ruby
  115. <body>
  116. <div class="header">
  117. <h5><a href="/">Home</a></h5>
  118. <% if current_user %>
  119. <h5><a href='/logout'>Logout</a></h5>
  120. <h5>Signed in as: <a href="/users/<%= current_user.name %>"><%= current_user.name %></a></h5>
  121. <% else %>
  122. <h5><a href="/login">Login</a></h5>
  123. <% end %>
  124. </div>
  125. <div class="container">
  126. <%= yield %>
  127. </div>
  128. </body>
  129. ```
  130. The above specifies that if the <code>current_user</code> helper method returns true (meaning, a user has logged in):
  131.  
  132. * display a "Logout" link
  133. * display that they are "Signed in as [username]" (and the username links to their profile page)
  134.  
  135. Otherwise, just display a link to the login page (and to the "create an account" page, if you've chosen to separate them).
  136.  
  137. ### Make the views
  138.  
  139. You know how to create forms, so I won't belabor the point here. Things to remember:
  140.  
  141. * When naming fields, match them to the database table column names (e.g.: <code>username</code>,<code>email</code>,<code>password</code>)
  142. * Get fancy and put them in a hash
  143. * <code>user[username]</code>,<code>user[email]</code>,<code>user[password]</code> will POST:
  144. * <code> params => {user => {username: < username >, email: < email >, password: < password > }</code>
  145. * **NOTE:** there is no colon (":") used in the input field names. It's just <code>user[username]</code>.
  146. * make sure your form actions match the appropriate routes!
  147. * Get in the habit of using <code>autofocus</code> in forms, usually in whatever is the first input field on the entire page. It makes your users happier, even if they don't realize it at the time.
  148.  
  149. ### Back to the controller
  150.  
  151. So now that data can be sent through, let's build out those POST routes.
  152.  
  153. New accounts are fairly straight-forward, since we're not doing anything with password encryption (BCrypt) just yet. Take in the form params and use them to create a new User, then set the session cookie:
  154.  
  155. ```ruby
  156. post '/new_account' do
  157. user = User.create(params[:user])
  158. session[:user_id] = user.id
  159. redirect '/'
  160. end
  161. ```
  162. You can choose to redirect them wherever it makes the most sense to you to send your users after they've made a new account. Maybe it's back to the home page? Maybe it's to their profile page (<code>redirect "/users/#{user.username}"</code>? Maybe it's something fancier? Totally up to you.
  163.  
  164. Logging in is a little tricker, since we'll need to make sure the user has submitted the correct password.
  165.  
  166. If your login form takes in a username and password, the process should go:
  167.  
  168. 1. Find the user based on <code>params[:user][:username]</code>
  169. 2. Check if <code>user.password</code> matches <code>params[:user][:password]</code>
  170. 3. If it matches, redirect the user to wherever (see above).
  171. 4. If it doesn't match, you'll probably want to just send them back to the login page so they can try again.
  172. 5. (You can get fancy and show an error message on the login page so the user knows why they've been sent back there!)
  173.  
  174. ```ruby
  175. post '/login' do
  176. user = User.find_by_username(params[:user][:username])
  177. if user.password == params[:user][:password]
  178. session[:user_id] = user.id
  179. redirect '/'
  180. else
  181. redirect '/login'
  182. end
  183. end
  184. ```
  185. Except, oh man, model code in the controller! This can be refactored to:
  186.  
  187. ```ruby
  188. post '/login' do
  189. if user = User.authenticate(params[:user])
  190. session[:user_id] = user.id
  191. redirect '/'
  192. else
  193. redirect '/login'
  194. end
  195. end
  196. ```
  197. ```ruby
  198. class User < ActiveRecord::Base
  199.  
  200. def self.authenticate(params)
  201. user = User.find_by_name(params[:username])
  202. (user && user.password == params[:password]) ? user : nil
  203. end
  204.  
  205. end
  206. ```
  207. This creates a User class method <code>authenticate</code> which takes in a <code>params</code> hash. The controller sends this method <code>params[:user]</code> as that hash.
  208.  
  209. Why a class method? Because you need to find a specific user, but you don't want to make the controller do that work. The model should do that work, but without a specific user (yeah, it gets kind of circular), you can't use an instance method… so you have to use a class method.
  210.  
  211. Speaking of! What is this class method doing?
  212.  
  213. The first line is trying to find a specific user based on the username that was submitted via the login form, and storing whatever it finds in the local variable <code>user</code>.
  214.  
  215. The second line is saying:
  216.  
  217. * **IF** a user was found (because if the submitted username didn't actually exist in the database, <code>User.find_by…</code> would have returned <code>nil</code>, which is the same as <code>false</code>
  218. * **AND IF** that found user's password matches the password that was submitted in the form
  219. * **THEN** this function will return <code>user</code>
  220. * **ELSE** this function will return <code>nil</code>
  221.  
  222. So if you look back at the refactored route, it's saying:
  223.  
  224. * If <code>User.authenticate</code> returns a user, store that user in a local variable <code>user</code>, set the session cookie for this user and redirect to the root path
  225. * Else redirect to the login page so the user can try again
  226.  
  227. ### Back to the helper method
  228.  
  229. Maybe your app has routes you only want logged-in users to access. I.e., only logged-in users can create blog posts or upload photos or submit links or leave comments (etc.).
  230.  
  231. Let's pretend this is a blogging app, and you have a route for the page that contains the form used to create a new post:
  232.  
  233. ```ruby
  234. get '/create_post' do
  235. end
  236. ```
  237. Because you have that helper method, you can do something like:
  238.  
  239. ```ruby
  240. get '/create_post' do
  241. if current_user
  242. erb :create_post
  243. else
  244. redirect '/login'
  245. end
  246. end
  247. ```
  248. which just says, if <code>current_user</code> returns a user, load this page and show the form for creating a new post. Otherwise, if <code>current_user</code> returns <code>nil</code>, redirect the user to the login page.
  249.  
  250. ### Bonus funtastical features to think about
  251.  
  252. * If a non-logged in user clicks on a link to a protected route (meaning, only logged-in users can see that page) and is redirected to the login page, and then the user successfully signs in… wouldn't it be nice if the user could be redirected to the page they were trying to access in the first place?
  253. * How can the app know when to display an error message?
  254. * Remember: you can store ~ 4 Kb in a session
  255. * If a user is logged in, are there still pages that user shouldn't be able to access? (Think about editing a profile page. Should users be able to access the profile edit page of other users? (Easy answer: no)).
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement