Closed watzon closed 3 years ago
I feel like this was discussed in an issue somewhere. I could have sworn I saw an interface that looked more like
class Users::Index < BrowserAction
subdomain ":username"
get "/" do
# Check for an existing user
html IndexPage, user: user
end
end
🤔
If not, then we would probably want to go this route anyway just so you can include it in a higher level or a module... Now the question becomes, do we allow for only 1 subdomain? What if we want 2 or more?
class Users::Index < BrowserAction
# Only access this route when you're on
# https://someuser.staging.members.mysite.com
subdomain ":username.staging.members"
get "/" do
# Check for an existing user
html IndexPage, user: user
end
end
OH! It was the route prefix I was thinking of.
I think allowing multiple subdomains would be nice. It would be kind of like route prefix, but for the host.
Ok, so to throw some thoughts out there on this, here's a few questions:
staging
, and qa
subdomains only, but based on which subdomain you use, we might render a different page....skip_before_pipe_subdomain_constraint
or whatever)Just a rough sketch, but maybe this would cover it?
macro subdomains(*domain_parts)
before :subdomain_constraint
{% for domain in domain_parts %}
# if domain is a variable, make a method by
# that name that takes the value from the request.host
{% end %}
def subdomain_constraint
domain = @context.request.host.as(String)
# some other complex logic
end
end
Are there any other edge cases you can think of?
In my opinion:
Does a user need access to know which subdomain hit this action? - yes
Do we restrict this action to the specified subdomain like a constraint? - yes
Do we render a 404 if you hit it otherwise? - yes
Do we add this to just a before pipe on any action that includes it? - probably, leaning towards yes
Nothing else I can think of personally. Seems like you covered the bases.
I've been looking at one of our Rails apps using a lot of subdomain constraints, and I think this includes everything. There might be edge cases but starting with this, you'll make 99% of the users happy.
The main thing we are going to start doing with all Lucky features is always make sure there's an escape hatch. So use this thing how we intended, but if it doesn't do what you want, here's out to break out of it. So as long as we follow that, then I think it should all be good.
Looking forward to seeing a PR! I'd do it myself, but I don't know near enough about the router internals or have the time :sweat_smile:
I'm working on this, but thinking about some stuff. In these examples, we're saying that parts of your app need a subdomain constraint, but really it may be more common for your entire app to be constrained. If that's the case, you don't want all of your forms and links to need to include the subdomain. You shouldn't have to think about it.
This means that it needs to be configured globally with an escape hatch for specific actions... Let's say your app is a browser web app located at app.whatever.com
, but your app has a few small api endpoints located at api.whatever.com
. If you do link to: Things::Index
, that should know app.whatever.com/things
. Then if you need to pull up the api, it should be Api::Things::Index.with(subdomain: "api")
...
Lucky::RouteHelper.configure do |settings|
settings.base_uri = "whatever.com"
settings.base_subdomain = "app"
end
class Home::Index < BrowserAction
#....
end
# should know about the "app" subdomain already
link "Home", to: Home::Index
# skips the "app" subdomain, and uses this subdomain
link "Admin", to: Admin::Index.with(subdomain: "admin")
# Also skips the "app" subdomain and uses the "admin" subdomain....
link "Admin", to: Admin::Index
class Admin::Index < BrowserAction
subdomain "admin"
#....
end
Here's where it gets tricky.... What if your subdomain is global, and dynamic? For example, shopify gives you a whatever.myshopify.com
address. So we know the base_uri
is myshopify.com
, but the base_subdomain
is dynamic... we don't know what that is. My apps kinda do the same thing in staging because we load all our sites off the same domain in staging, then use the subdomain as the site lookup. In this case, do we just say that you ignore setting the value in the RouteHelper, and only set in the BrowserAction?
# if the subdomain is cheese.myshopify.com
# This should know about the cheese subdomain
link "Home", to: Home::Index
Ok, so maybe it's stored in session then? But this also means that cookies should be tied to that subdomain. You wouldn't want to store them on *.myshopify.com
otherwise logging in to one store would have you logged in to another. So in this case, you'd want all cookies to be tied to that specific subdomain. How does that look with the session stuff? Does this subdomain thing need to take a block and pass context?
class Home::Index < BrowserAction
subdomain ":storename" do
# whatever the cookie thing is....
context.cookie.domain("#{storename}.myshopify.com")
end
end
You just had to go and use your brain and make things all complicated like :joy:
Lot of good thoughts here. I've got a few thoughts.
I've always considered routes to conceptually cover the path of the url. Everything after the /
, like the /foo/bar
in https://example.com/foo/bar
. The domain (or subdomain) is everything before that /
. Because of this, I've never been a fan of Rails' constraints because mix domains and paths. I love @jwoertink's suggestion to make it separate from the route definition.
I also don't love the concept of a "subdomain" over just "domain" or "host". DNS does not discriminate. example.com
, foo.example.com
and bar.foo.example.com
are all domains. The parts are subdomains, but it's a bit odd if your site is del.icio.us
; the concept of a subdomain over a domain starts to break down.
Separating that idea may make some of these concepts cleaner and easier to implement (and abstract) because we're treating and modeling them like browsers and the internet treats them.
I'm not sure how possible this is, but I'd love to see domains/hosts live at the action level. Then links and child actions and cookies can pick up on them.
Example multi-tenant site (like shopify) where I have a main site and variable subdomains for each customer:
class BrowserAction
host "store.com"
end
class UserStoreAction
host ":username.store.com"
end
Then inside of a UserStoreAction
link "Home", to: Home::Index
can pick up that Home::Index
is a BrowserAction
and redirect to the proper host, but link "My Products", to: Store::Show.with(store: current_user.store)
will also know which host to go to.
I'm not sure how cookies would work. Perhaps they have to be moved to actions as well.
I actually like those thoughts, especially when you take into account that some multi-tenant sites actually give you the ability to specify your own domain, not just subdomain. It would be nice if you could have full control over the entire thing and not be constrained to a single domain with the possibility of multiple subdomains.
host
seems like a perfect solution to me.
Yeah, my app is multitenant with multiple domains, and subdomains. Like we have t.com, e.com, c.com, and then we have t.staging.devsite.com, e.staging.devsite.com, and c.staging.devsite.com.
Then we have a before pipe that does
def current_site
host = @context.request.host.as(String)
name = case Lucky::Env
when .development?
when .staging?
when .production?
else
end
SiteQuery.new.name(name).first
end
Now, I'm totally on board with what @edwardloveall is saying (also, nice name drop on the domain), but in this case it wouldn't work for my app unless that host
took a method name, or you could do....
class BrowserAction
host ":the_whole_thing_is_dynamic"
end
Because we have o.com
and o.net
. Now, with that said, we already have Lucky::RouteHelper
that you configure to set your base_uri anyway. So you can already do Lucky::RouteHelper.settings.base_uri
to get that static value.
I think where I'm getting a little foggy in is the concept of you can't hit the route unless that specific subdomain matches. I think what I'll do is I'll hammer out my thoughts in to code, then push up a WIP and ping y'all for a review. We can all hash it out with actual code to make it easier to see possible pitfalls or whatever.
Thanks for the help!
Can't wait! Thanks for being so proactive, both of you!
@jwoertink Thoughts on this being a 1.0 item to include? Not currently on our roadmap doc, but one of those things that I think most folks will expect frameworks to have an option for.
Yeah, I actually started working on this a while ago, but so many other things came up I forgot to get back to it. We can put this on the 1.0 roadmap as a nice to have. It'll for sure make it in, just not sure if actually in 1.0, or just after.
I think it might be getting too far into the weeds to go all the way to validating the host. I was looking around at how other frameworks solve this issue. Many do provide support for the whole host, but I came across https://github.com/fnando/sinatra-subdomain and think it's a fairly effective way to provide basic subdomain support without mucking around with a bunch of other stuff.
It could look something like:
class Users::Index < BrowserAction
include SubdomainLibrary
subdomain :admin
get "/users" do
# limited to admin.myapp.com/users
end
end
There could be different ways of calling subdomain
In the route block there would be a subdomain
method you could call to get the value of the subdomain.
I'm thinking when multiple subdomains are passed "e.staging.devsite.com" that the subdomain is "e.staging".
I think this covers 99% of use cases without being overly complicated.
I forgot that I made this https://github.com/matthewmcgarvey/lucky_subdomain
@matthewmcgarvey this is awesome! Still would love to see something like this built into the framework, but this is great.
Matthew you beautiful son of a gun. Well done!
Thought there was already an existing issue, but I couldn't find one. I guess we've just talked about it in the gitter chat and it was never brought up here.
The problem
Subdomains are pretty commonly used to separate pieces of a site. You may want to give each user a subdomain based on their username, add subdomains for different regions, etc.
With Rails they have the notion of constraints, which allow you to constrain a route using different parameters, including a subdomain. For a dynamic subdomain you just subclass
Constraint
and put your resolving logic in there.Solution?
I have a couple potential ideas for a solution. The first of which doesn't require many changes to the way things are already done. Consider the following action:
A simple way to handle subdomains would be to just add an optional
sub
orsubdomain
parameter to the existing routing macros. So the if I wanted to change the above code to use a subdomain instead, maybe we could do something like this:or for a non-wildcard type domain:
Another potential solution would be to do the Rails'y thing and use the concept of constraints. This would take a bit more work, but could allow routes to be further constrained using validations. How this could work would take a bit of discussion, but it would also make routing in Lucky even more powerful.