(2020-10-12) Building user management in WikiFlux

Building user management in WikiFlux so it will be multi-user.

What kinda flow do I want?

  • self-service to create
    • create "user"
    • then they log in
    • then pay, which then activates user which allows actual creation/editing (and links/redirects into the space)
  • login: hit link on any page, submit, get redirected back to self? Or root, I'm lazy.
  • view-vs-edit, privacy, etc.
    • can view public page anonymously, have login link in menu as today.
    • when viewing private page, or rendering Edit link for a view-public-page, have to check that the user is the owner of that space!

Oct12

  • pip install Flask-User → success
  • going to have to manually alter the users table schema.
    • am I going to use the role feature? Or do something nastier to limit admin rights to my 1 user?
    • probably keep it simple to start
    • for users table, make a SQL script I can run later in prod. Actually 2 bits, because have to modify the existing record before adding constraints. They're saved in file, but I ran them in command-line.
  • edit models.py
  • launch app →
class User(Base, UserMixin):
 NameError: name 'UserMixin' is not defined
  • made some tweaks, changed to
    'No Flask-SQLAlchemy, Flask-MongoEngine or Flask-Flywheel installed and no Pynamo Model in use.'\
flask_user.ConfigError: No Flask-SQLAlchemy, Flask-MongoEngine or Flask-Flywheel installed and no Pynamo Model in use. You must install one of these Flask extensions.
  • so it's not seeing the db connection. I wonder if I need to change my other code to use db = SQLAlchemy(app) but that probably requires a bunch of other changes. Maybe need to find some more example Flask apps, conform my code?
  • Oct18 hacked the flask-user db_manager code to assume it's sqlalchemy

Oct18

  • added email-config values - use bill@simplest-thing.com
  • it didn't like my secret_key, I had to muck around to satisfy it
  • now app is running!
  • paste in code for /members route
  • hit /members get sign-in page; enter info I had pushed into db before → get error, derp I added column email_confirmed should be email_confirmed_at
  • ALTER TABLE to fix column; hit again → UnknownHashError: hash could not be identified hmm not lots of help there
  • try registering new account bill@simplest-thing.comAttributeError: 'scoped_session' object has no attribute 'session'
  • hack couple lines in flask-user to turn db.session. into db.
  • submit new-user again → fails because flask-user isn't creating id - null value in column "id" of relation "users" violates not-null constraint
  • while my code refers to field as auto-increment, nothing in the schema creates that.
    • hmm should I use field type serial? How convert to that? Nope, user more SQL-compliant identity/generated
    • once I settle on type, I can probably drop the existing id field then add the new type
    • decide on this - create sequence

Oct19

  • fix schema ^^
  • submit again - realize form is missing lots of fields I made required. Do I really want the user to have to enter them all to create an account? Probably....
  • derp they have a built-in template, I'll try the standard syntax to use that template!

Oct20

  • the built-in template only uses the couple fields, too. Gonna make local copy and add my fields. No still seems to be using their form.
  • change my route to /register, now get an error about form object for hidden_tag - research says that's for WTForms which is for CSRF protection, so I should be doing that anyway.
  • step back a sec - confirm app is working fine still with old hard-coded admin login, so I have a safe base to work form. So going to put in WTF/WTForms first. Get that working with old-model, then move forward.
  • pip install flask-wtf → I think it was automatically installed by flask-user
  • put in some code, but template is still unchanged.... it works
  • add in form.validate_on_submit() part - dies hard

Oct21

  • changed it to just use validate yeah it fails in there
  • ah probably forms class has to have fields matching what's in my manual form derp
  • still failing
  • derp had to put in {{ form.csrf_token }} in my form, boom!
  • (also confirmed making-new-page works)
  • next - back to Flask-User

Oct22

  • got to next error, I need to define the form class
    • I can extend the RegisterForm class (to add fields), or copy/paste/fork. I'm going to try extending.
  • tried forking, couldn't figure out how to import validators to call, etc. so went back to extending
  • finally got form to load
  • but have no method to submit yet

Oct23

  • before I add any user records, I need to get that serial/generated thing working.
  • ok it's done (did I just do it, or the other day?) - did pgdump of schema
  • add more fields to schema, model, form, field
  • next: POST logic
  • unclear from examples how much is manual to get the form data, and then save to db
  • try to see if validate_and_submit() does the job, appear not.
  • ah, key method is populate_obj()
  • but fail: was a class (models.User) supplied where an instance was required? - just needed parens
  • now running but not inserting

Oct25

  • ah print user says the populate_obj() call isn't actually doing anything....
  • oh derp actual problem was that I wasn't calling commit!
  • now have issue with username is empty, despite form... (which then triggers uniqueness issue)

Oct26

  • try ripping out bits of form logic, still no change. Other fields (both built-in and added-by-me) are handled fine.

Oct27

  • realize I wasn't following the right approach to customizing form templates. Move my custom form, change code to map to it. Form loads, but now submit passes all null data.
  • derp some of the doc pages I've been using are for 0.6 instead of 1.0!
    • different code change
    • nope all still null!
  • uncommented form.populate_obj(user) - now getting fields except for username again
  • trying adding username to MyRegisterForm() - didn't help, took it back out
  • asking for help in github

Nov01

  • wondering if I should step back and align my code's structure with a more "standard" Flask model. But what the heck is that?
  • Do I have to adopt Blueprints?

Plan: I'm going to suck it up and upgrade to current Flask, then restructure in line with this

Nov03

  • pip install --upgrade Flask no complaints. Runs fine!
  • tutorial puts a venv inside the app's directory, then an app in there. I'm not putting the venv down at that level. But I will make an app subdirectory of my app's directory
  • make wikiweb.py above app with just from app import app
  • make __init__.py inside app with the app = Flask(__name__) and similar bits
  • rename old core file wikiweb.py to routes.py. Rip out the main caller bits, swap in from app import app for past app-import bits.
  • set environment variable from command-line
  • flask runflask_user.ConfigError: Config setting USER_EMAIL_SENDER_EMAIL is missing. even though it's there in the routes.py file. Should probably move some config stuff around.
  • so jump ahead to section 3 - make config.py above /app/
  • flask run → launches! Hit page - works!
  • go back to section1, do pip install python-dotenv and make .flaskenv file. Still runs
  • section2: I already have a templates subdirectory with templates, and they are working since my pages are loading.
  • section3: I have WTForms, but maybe now's a good time to play with that directly for a non-user/login form...
  • create new page, leave body empty, submit → "Internal Server Error", see my form.validate() was false, with form.errors = {'body': [u'This field is required.']}

Nov04

  • ah, I see, they use validate_on_submit() so that if validation fails they can treat it like a GET and deliver the form. Would need nice error handling, but that kinda makes sense....
  • tweak to use that → happy paths work; missing-field silent (to user) but doesn't explode
  • I already had flash-messages in my layout template, now call flash if validate fail

Nov05

  • section iv - databases
  • switch to db instead of db_session, stop importing the database.py file that calls scoped_session, move config bits into config.py
  • launch → get error on user_manager = CustomUserManager(app, db, User) because db is not defined - suspect this is part of the kludging I did to get flask-user working, so will go back to their raw instructions....
  • follow their starter-app organization - move UserManager bits into __init__.py
  • now launches. Hit page → db not defined - still need a from app import db in routes.py
  • then need to comment out the db shutdown/remove bits
  • now working - to view page
  • edit → 'SQLAlchemy' object has no attribute 'commit'
  • change all the db.commit() to db.session.commit() → no error, but it also doesn't actually save the change!
  • added SQLALCHEMY_COMMIT_ON_TEARDOWN = True to config.py → edits seem to save, but
    • when I re-edit the changed page, the body field has <p> tags?!?
    • and saving a new page doesn't work - db.add(node)'SQLAlchemy' object has no attribute 'add'
  • make it db.session.add(node) → saves

Nov06: what's with the <p> tags?

  • seeing it for a bunch of pages
  • hrm it's actually in the db!
  • but only in the top few - weird have I had that bug for awhile?
  • no actually only a couple pages - viewing the page is enough to make it happen!
  • I think it's the "magic" library logic, combined with me over-writing the object value (but not explicitly saving that back!)
  • made that a new variable, called it in template → now all good
  • but now realize that all my edited pages have modified=null
  • it's failing my 2 action value cases, falling into a last case I just use for remote-POST, and therefore not setting modified.
  • printing full response.form shows I have 2 action entries, one of which is empty?!?
  • can't find where this is coming from
  • maybe having a form param named action is a bad idea? (It hasn't been an issue in the past. But maybe doesn't play well somewhere in magic now....)
  • ah, it's not just that field! It's happening with another field which is defined as HiddenField in my forms.py class?!? (Since my template is old, all the fields in it are explicit

Nov11

  • just make those field StringField instead of HiddenField - now setting Modified

Nov12

  • back to Flask-User (diverted as of Nov01)
  • null username issue again/still
  • pondering: why do I even need a username if I'm going to have people user their email for authentication? Maybe I should just make the field optional in the db, and move on?
  • part of my confusion is USER_ENABLE_EMAIL and USER_ENABLE_USERNAME config settings... do you have to set just 1 or the other? Does me having u_e_u set to False cause the code to ignore the form field?
  • set them both to True → it worked! Record created with value filled in!
  • but what will happen if I try to log in?
  • try, but was using random password and didn't save it, so rejected! Or is it something else? Actually page crash with UnknownHashError: hash could not be identified
  • try Forgot-Password feature → SMTP Connection error: Check your MAIL_SERVER and MAIL_PORT settings
  • paste/fork Flask-Mail SMTP server settings → email sends, I receive it
  • click link in password-reset email → 'SQLAlchemy' object has no attribute 'commit'

Nov16:

  • change flask-user sql_db_adaptor.py to use db.session.commit() - had changed that when I was first trying to get flask-user working weeks ago.
  • password-reset → works, receive the email. Click, get form. Enter new password (and save it this time). Saves successfully! (Though it also says Your account has not been enabled.)
  • submit email/password - says Your account has not been enabled. I'm guessing that's because db users.active is null
  • log in to the other account, which I had manually set to active=t, and login is fine.
  • noting that the tutorial series I've been following users Flask-Login instead of Flask-User grrr
  • will play with mapping of user to space before getting around to Stripe integration
  • oh heck just realized my whole user-namespace thing is based on subdomains, so I need to map a domain to localhost, can't use 0.0.0.0. Done.
  • found subdomain-route code
  • found filter AND syntax
  • realize that I never created any spaces upon creating the user.
  • Now wondering if I really need/want to have that.
  • if I force everyone to use /private and /wiki for their space names, it becomes unnecessary. (Or I could invent new defaults for everyone else, like /garden instead of /wiki, and handle mine as a special exception.)
  • hrm and spaces.path is redundant to users.subdomain
  • ah but spaces.id is foreign key in nodes.space_id, so I'd need to refactor that. And maybe I'll want to offer some people a 3rd space, etc.
  • so....
    • in registration form I should ask for path and title for each of 2 spaces, and create them when creating the user
    • and change my spaces.path to refer to the url-path rather than the subdomain
    • have to re-order my logic to look up path->space, and then get its spaces.privacy_type

Nov17

  • had to put specific hostnames into my hosts file because it doesn't support wildcards for subdomains
  • had to put SERVER_NAME for domain into config (I think)
  • now I clearly have to go re-learn Flask-SqlAlchemy query/filter/join syntax
  • ah my tutorial series has a post that covers that
  • got the queries working (for view-1-page), now have to fix templates
  • have template content rendering, but wrapper bits are a mess - I think my static route handling fell apart in the refactor, though I thought I would have seen that affect sooner....
  • seems like static-path URLs aren't getting handled - connection refused - http://flux.garden:5000/static/bill2009_152.png - or, if I add a subdomain, then it gets picked up by one of my routes!
  • oh derp - when I added the subdomain cases to my hosts file, I left out the raw-domain case! So once I added that line to my hosts file, all good!

Nov18

  • hit a private page → NameError: global name 'is_anonymous' is not defined. Derp, that's User.is_anonymous
  • now recognizing have to login, redirects to /user/sign-in but that is still within the subdomain, so it gets grabbed by the route and fails - need to redirect to the raw-domain. Ah, should use url_for.... ah, arg for that needs to be in quotes. Now good!
  • tk - redirect or at least link to source page after successful login
  • reload private page - get redirected to sign-in then instantly redirected to sign-in-successful page
  • ah, it's not User.is_anonymous, it's current_user.is_anonymous - I thought that smelled wrong. Now it's good.
  • now get edit link to render. Change to use is_authenticated, generates link but it's wrong. Running into issues with using subdomain model.
  • include subdomain in call to render_template() → link in template url_for('node_edit', page_url=page_url, path=path, subdomain=subdomain) rendered to http://flux.garden:5000/private/2019-09-05-Jangly/edit?subdomain=webseitz
  • posted to stackoverflow.

Nov20

  • go no answer to url_for issue, so just making my own relative URL reference. Works.
  • hit edit link → getting matched to the page-view route instead of page-edit. Because haven't added the subdomain handler to the edit route+function. Also copy in other bits of refactoring from the page-view function.
  • hit edit → page container, but no form
  • rip out logged_in() conditional, fix functions to for new args, etc. → get form
  • submit edit → Saved! But redirect failed, added subdomain again
  • (and various other tweak) → edit saves, redirects to page-view all good
  • did the new-page variation → worked!
  • on to FrontPage: couple tweak → worked!
  • (actually, not true, it's not checking user yet, it's still the old logic there)

Nov22

  • hmm, I'm going to manually create the 2 space records for user2, make sure the page view/edit bits are working, then hit the FrontPage. (see Nov16 notes)
  • created spaces records - going to have issues with setting ID-increment-start-point for this table, too

Nov23

  • doing page-view - seems like favicon.ico is getting picked up by routes other than static, even though other static images aren't having that issue, and this file wasn't an issue with user_id=1.
  • Feels like I was failing-lucky before. Need to add url rule - but it's unclear where that goes (I had this in my old structure).
  • tried public/outer page-view (realized I was hitting a private, which adds complexity) - discovered Space lookup needed a lower() wrapper, fixed that.
  • now getting favicon error, but a different one, triggered by cascading redirects
127.0.0.1 - - [23/Nov/2020 11:12:29] "GET /favicon.ico HTTP/1.1" 308 -
127.0.0.1 - - [23/Nov/2020 11:12:29] "GET /favicon.ico/ HTTP/1.1" 308 -
[2020-11-23 11:12:29,086] ERROR in app: Exception on /favicon.ico/FrontPage [GET]
  • yikes cascading stupid, realize had space.privacy_type set to the path, not the privacy_type
  • then realized that the new user was id=16 not id=2 so the spaces weren't assigned right
  • now getting redirected to sign-in page
  • more derp I had set the wrong privacy_type for the space. Now rendering the page.
  • now realize I'm checking the user is logged in, but not that they own that space!

Nov24

  • check that user owns page to view-private-page
  • have editing working in appropriate cases also
  • trying FrontPage variations
  • my outer FrontPage loads fine (after tweaks)
  • new-space outer FrontPage: nope it expects sidebar pages to exist already
  • created fallback empty-strings → works! (and sidebar-items link to page-view/edit url to create)
  • new-space inner FrontPage also works
  • hmm have some things hitting a domain-top-root - What will be there? A pitch (SplashPage)? A list of (public) spaces? Both? OK, had commented out a route for that, put it back, tweaked that page to remind myself of what goes there, and how it fits into various flows....
  • next: RSS feed... working!

Nov28

  • title-search tweak → working

Nov29

  • space-owner-id=1 stuff has to be moved from global into something else....
  • focus on view-node route first → working (though style doesn't look right for other users, have to dig in after doing other routes)
  • did other page → good

Dec01

  • next - styling that looks wrong - different font from the body downward, though the H1 looks the same.
  • did view-source of each, pasted into HTML files did diff on them, found only the expected differences.
  • checked Safari as well as Chrome and got same result. Grrr
  • Inspect element - the "wrong" style is getting e-content style from default.css, while the normal/expected is getting that overridden by cherwell.css
  • oh derp, missed that line was buried in a chunk of "expected differences", for some reason that line was adjacent to a chunk of IndieWeb stuff I'm commenting out for non-me users. Moved → now good!

Dec01

  • create stripe.com account
  • plan to use Checkout with subscriptions - use their hosted forms

What's left?

  • Stripe → activation flow
    • including creation of spaces - need to add fields to form and store them. Do that in the initial register form, or separate/later? Probably collect the data at register form, use them in creating the spaces, so don't need to store separately, and then just activation happens after Stripe.
    • hrm suspect I'm checking various things about space/user, but not is_active
  • auto-post to WikiGraph service/db for Visible Backlinks. (Which belongs on a page of its own.)

Dec11

  • realize that I had never un-borked the registration code. It still had the db/session mess. Now fixed.
  • add fields for 2 spaces to the registration form. Add add-space logic. Submit user → creates user, but no spaces.
  • derp need to add that to models import
  • did that → still not creating spaces
  • realized I should use form.inner_path instead of user.inner_path
  • noticing I'm doing things like db.session.add(outer) (and commit) instead of outer.save() - so try changing it. No difference.

Dec13

  • turn on debug mode, noticing that my print statements haven't been doing anything the last couple days.... Nope still not print ouput.
  • But I do notice a key field I'd left out in setting. Add that → nope still not enough.
  • I wonder whether my psql server has a nice log to view...
  • ask the db for log location. Find log, but no entries since Nov23?!? Probably not turned on since I moved to new machine. Turn logging on. Resort to log_statement. Stop/start service. Nope, no insert-space queries - so there's not an error there that's not getting passed back.
  • after a bit more flogging, post question

Dec14

  • got a suggestion in StackExchange, but it didn't help. Trying in discord

Dec15

  • tachyondecay at discord noted the key relationship line in my models.py was commented out. Probably during early-state problems. Uncommenting, plus tweaking: spaces = relationship('Space', backref='owner', lazy=True). Also adding SqlAlchemy_echo to config. Nope still only inserting user.
2020-12-15 19:49:54,563 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.username AS users_username, users.subdomain AS users_subdomain, users.email AS users_email, users.email_confirmed_at AS users_email_confirmed_at, users.password AS users_password, users.active AS users_active, users.first_name AS users_first_name, users.last_name AS users_last_name 
FROM users 
WHERE users.username ILIKE %(username_1)s 
 LIMIT %(param_1)s
2020-12-15 19:49:54,563 INFO sqlalchemy.engine.base.Engine {'param_1': 1, 'username_1': u'Test14'}
2020-12-15 19:49:54,588 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.username AS users_username, users.subdomain AS users_subdomain, users.email AS users_email, users.email_confirmed_at AS users_email_confirmed_at, users.password AS users_password, users.active AS users_active, users.first_name AS users_first_name, users.last_name AS users_last_name 
FROM users 
WHERE users.email ILIKE %(email_1)s 
 LIMIT %(param_1)s
2020-12-15 19:49:54,588 INFO sqlalchemy.engine.base.Engine {'email_1': u'fluxent+14@gmail.com', 'param_1': 1}
2020-12-15 19:49:54,855 INFO sqlalchemy.engine.base.Engine INSERT INTO users (username, subdomain, email, email_confirmed_at, password, active, first_name, last_name) VALUES (%(username)s, %(subdomain)s, %(email)s, %(email_confirmed_at)s, %(password)s, %(active)s, %(first_name)s, %(last_name)s) RETURNING users.id
2020-12-15 19:49:54,855 INFO sqlalchemy.engine.base.Engine {'username': u'Test14', 'first_name': u'Test14', 'last_name': u'Test14', 'email_confirmed_at': None, 'password': '$2b$12$o6A8jDSA6vwsBYGqrUMHQO7uFzqovXqRcP3Pfv5Ah7NawcYY86aRa', 'active': True, 'subdomain': u'Test14', 'email': u'fluxent+14@gmail.com'}
2020-12-15 19:49:54,857 INFO sqlalchemy.engine.base.Engine COMMIT
2020-12-15 19:49:54,858 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2020-12-15 19:49:54,858 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.username AS users_username, users.subdomain AS users_subdomain, users.email AS users_email, users.email_confirmed_at AS users_email_confirmed_at, users.password AS users_password, users.active AS users_active, users.first_name AS users_first_name, users.last_name AS users_last_name 
FROM users 
WHERE users.id = %(param_1)s
  • FYI here's current libraries.
appdirs==1.4.4
configparser==4.0.2
contextlib2==0.6.0.post1
distlib==0.3.1
filelock==3.0.12
httplib2==0.18.1
importlib-metadata==2.0.0
importlib-resources==3.0.0
oauth2==1.9.0.post1
pathlib2==2.3.5
scandir==1.10.0
singledispatch==3.4.0.3
six==1.15.0
typing==3.7.4.3
virtualenv==20.0.32
zipp==1.2.0
  • after lots of thrashing, it seems most likely that my register method isn't even being called, but that the default Flask-User view is being used! Derp of the year. Really questioning whether I want to keep this magic, or switch to Flask-Login...

Dec18

  • so what are my options moving forward?
    1. rip out Flask-User and go with Flask-Login
    2. edit the Flask-User register view code in-place (which could be issue when I build something unrelated)
    3. copy/paste the root code in to my CustomUserManager and modify it there
  • I'm leaning toward opt3. Ok let's do this.
  • have to add some from flask import bits
  • ah, sequence issue on spaces.id - do alter sequence spaces_id_seq restart with 5 - sets last_value=5 - which will actually be the next value
  • submit → global name 'signals' is not defined - add signals to from flask import line
  • submit → AttributeError: 'module' object has no attribute 'user_registered' - also associated with signal. Decide to just delete that line, since I don't think I'll need signals.
  • yes, everything worked!
  • but want it to finish by redirecting to a different/special/login page
  • set USER_AFTER_REGISTER_ENDPOINT = 'user.login' in config
  • and make a home page post-login to check user status (paid, confirmed). so set USER_AFTER_LOGIN_ENDPOINT = 'home'

Dec19

  • make home page
  • notice all my users have active=t, why is that being set automatically?
    • I want to set user to active iff (a) email_confirmed and (b) paid
  • change a user to active=f, login → Your account has not been enabled.
  • this seems baked deep into Flask-Login, not just Flask-User. I could try to change that, but it smells like a bad idea.
  • So I'll just adjust my page-flow so that Login is the last step.
  • Actually, I don't think I'll have to change flow, I'll just add some explanation to the Login page.

Dec20

  • changed 'home' node to just redirect them to their Inner space FrontPage (if active, etc.)
  • added link from inner to outer space in FrontPage
  • next: for private pages that trigger login, pass that page along so that user gets redirected to original-request after login. Argh, next argument from Flask-Login doesn't nicely handle subdomains. Or maybe I'm doing it wrong. See discussion.
    • see also Nov20 notes above around url_for and subdomains
    • for that issue, see code around app.url_build_error_handlers.append(external_url_handler)
    • but going to table this for now, it's not crucial
  • next: trigger Stripe payment

Dec21

  • install CLI for webhook testing
  • up to Step 4 Create a Checkout Session
  • paste and tweak route for create_checkout_session, create routes for pay_success and pay_cancel
    • going to put those templates into /flask_user/
  • copy entire script.js into static - but what calls it? Does Stripe just assume it's at root? Might have to move it.
  • copy the rest of the bits in that page, but don't understand any of it, and suspect it doesn't fit together.
    • though I do see the bits in the webhook for even when invoice_paid etc
      • should probably be storing some payment info in the user record (or in a payments table), and have some sort of method for updating user.active
  • looking for some Flask-specific docs/examples
    • Stripe has a legacy page made obsolete with Checkout in Apr'2019, so probably not that
    • there's a video
    • but let's try this page, following "fixed price" model.
  • starting this off by moving to .env file for environment variables, and use python-dotenv to load them
    • more precisely, use .flaskenv for Flask CLI configuration, like FLASK_APP etc (find put those in our .flaskenv file in article), and use .env file for passwords and such
    • but I don't think I need to put my price_id in the env file because of I have 2 prices, and I think that all happens automatically
    • stripe doc refers to script.js, this article has main.js which seems to be the same (and is in static), so I'm renaming mine to stripe.js
    • stopping to read ahead at the Authentication section - decide not going to add any db stuff until I get a skeleton working.
  • back to core Stripe documents. Also going looking for flow-diagrams
  • duh realize that Stripe doesn't offer a product page or shopping cart, just the payment. So I need a page listing the prices, but a button for each.

Dec22

  • smells like if user is not active then he doesn't get logged in, so no current_user defined, etc. Which doesn't work for passing along email to Stripe, etc. Which makes it smell like
    • I should just let new accounts be set active=True
    • I should set USER_ALLOW_LOGIN_WITHOUT_CONFIRMED_EMAIL=False to put that block in place
    • then I need a post-Login page which checks payment status, gives them subscription buttons if not paid, etc. (if permitted, then redirect them to private space) - I'll call it home
    • and I'll have a users.permitted field to track whether confirmed and current-paid
      • which circles back to how I track payment status with Stripe. They send invoice.paid or invoice.payment_failed events to my webhook
      • I should probably store every payment in a table. Or, really, every payment-event.
      • when I receive a checkout.session.completed event I set users.permitted=True
      • when I receive invoice.paid I set users.permitted=True
      • when I receive invoice.payment_failed I set users.permitted=False - I don't have to track when last-payment made, when due for next payment, etc., Stripe does all that for me.
      • when receive customer.subscription.deleted (means they canceled), set users.permitted=False
      • if permitted=False and confirmed=True then I check my payment_events table to give user feedback
      • do I store the Stripe customerId on the users table or the payment_events table? This tutorial makes a stripe_customers table - but won't I have 1:1 from User to StripeCustomer? And both 1:1 to Subscription? Or maybe if someone changes payment-model they get a different SubscriptionId? I guess I'll follow that model.
      • but I still need a table to store events, I think.
        • my own id, plus a source, plus a source_event_id_id for Stripe's id (planning ahead); plus a SubscriptionId or CustomerId?
        • maybe a status or is_handled? Nah, probably won't have a separate/async process, so everything will be handled as it's received
        • a datestamp
        • an event_type field
        • a blob field to store whatever else comes, as JSON
        • duh realize I want this to be a general-purpose events table (so went back and added field source above

Dec26

  • update models.py, sql schema per above

Jan03

  • mind-reset: write Using Stripe Checkout For Your SaaS Subscription
  • try a user login (test-06): great, it rejects login because email not confirmed.
  • re-send confirmation email; it redirects me to root page rather than login, but I guess that's ok since next action is really to go find that email anyway.
  • click confirm link - it updates db, but redirects me to root again. Want to see if that's changeable... yep change
USER_AFTER_CONFIRM_ENDPOINT = 'user.login'
USER_AFTER_RESEND_EMAIL_CONFIRMATION_ENDPOINT = 'user.login'
  • yes, now after confirmation get redirected to /home (because have USER_AUTO_LOGIN = True)
  • right now my Home page is showing 1 Subscribe/Pay button with <button id="checkout"> and clicking it does nothing.
  • turn that into 2 buttons, with id monthly-btn and yearly-btn
  • in my .env file put MONTHLY_PRICE_ID and YEARLY_PRICE_ID API Ids that Stripe assigns
  • then in my routes.py I define @app.route("/setup")... def get_publishable_key(): to return those Price IDs
  • then modify stripe.js (which I renamed from their script.js) to fetch the appropriate price-id and redirect to Stripe.
  • and... I see 2 buttons... but clicking does nothing. Derp my home.html template needs to reference the 2 JavaScript files - I had only done this for login.html
  • and... still nothing. View-source shows the JavaScript lines, clicking on my local stripe.js loads that file fine....
  • I see /setup in the console getting called and returning the price-ids
  • look in JavaScript console: Uncaught (in promise) IntegrationError: Invalid value for Stripe(): apiKey should be a string. You specified: undefined. Ah, I was returning publicKey instead of publishableKey.
    • sidebar issue: console now notes that live integrations must use HTTPS/SSL which I don't have running on my server. tk
  • now not working because /create_checkout_session route is giving a 400
    • I think it's saying "global name 'CHECKOUT_SESSION_ID' is not defined" (I went into chrome dev/devtools, picked "network", then clicked on the red create-checkout-session)
    • realize the error is being hidden because all this is inside a try - move variable-setting bits outside, confirm issue is global name 'CHECKOUT_SESSION_ID' is not defined when trying to attach it to success_url
    • stop using url_for and use
success_url = app.config["SERVER_PROTOCOL"] + app.config["SERVER_NAME"] +'/pay_success?{CHECKOUT_SESSION_ID}'

→ works!

Feb06 getting my Linode server caught up

  • pip install --upgrade Flask → lots of warning about InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. You can upgrade to a newer version of Python to solve this. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.
  • python is 2.7.6
  • doing nothing about it quite yet
  • pip install --upgrade pipSuccessfully installed pip-21.0.1 (with same warnings)
  • do pip install pyOpenSSL ndg-httpsclient pyasn1, → sys.stderr.write(f"ERROR: {exc}").... SyntaxError: invalid syntax which is not surprising: pip 21.0 dropped support for Python 2.7 and Python 3.5. Hmm maybe I'll downgrade to pip 20.3.4... → invalid syntax
  • tried using python get-pip.pyinvalid syntax?!?! Ah tried again with 2.7-specific flag, now seems ok (though still giving the original urllib3/ssl InsecurePlatformWarning)
  • so tried again pip install pyOpenSSL ndg-httpsclient pyasn1 - get warnings during the install, unclear whether it matters. Going to move forward....

Feb07

  • pip install python-dotenv → successful, but with InsecurePlatformWarning
  • pip install Flask-UserCouldn't find index page for 'Flask-Login' (maybe misspelled?)
  • pip install Flask-LoginSuccessfully installed Flask-Login-0.5.0
  • pip install Flask-User again → same error (2nd line is No local packages or download links found for Flask-Login - which is why I thought installing Flask-Login directly itself would solve the problem)
  • ls of dist-packages shows flask_login and Flask_Login-0.5.0.dist-info
  • try pip install Flask-User --no-dependencies → same error

(2021-02-07) Migrating to Python3


Edited: |