standardfile icon indicating copy to clipboard operation
standardfile copied to clipboard

Subscriptions

Open SerialDestructor opened this issue 2 years ago • 10 comments

The developers of StandardNotes have provided an official way to enable the 'Pro' subscription features on self-hosted instances:

https://docs.standardnotes.com/self-hosting/subscriptions/

Is it possible to build this into your server?

SerialDestructor avatar Apr 02 '22 16:04 SerialDestructor

It may be implemented with a fake API path returning the expected "Pro plan" payload to the frontend rather that implementing the whole subscription feature which has no meaning for this server. But it will take hours of work to analyze and support this feature only for three "Note Types" which I don't really need. (An nginx in front of the server should also be able to return the expected payload)

There is also this statement in the doc to consider:

Building Standard Notes has high costs. If everyone evaded contributing financially, we would no longer be here to continue to build upon and improve these services for you. Please consider donating if you do not plan on purchasing a subscription.

For the moment I will try to fix the refresh session issue and other issues.

PS: This project is opened to contributions.

mdouchement avatar Apr 04 '22 08:04 mdouchement

I looked into it. The StandardNotes app tries to retrieve the subscription data from v2/subscriptions. This is a valid response:

{
  "meta": {
    "auth": {
      "userUuid": "01170b7c-6b5c-480d-b3dd-2a619195fc56",
      "roles": [
        {
          "uuid": "e3760e5a-7d10-417a-aa68-df3afa3a0dab",
          "name": "PRO_USER"
        },
        {
          "uuid": "6468d154-36aa-453d-baf5-708d221351c3",
          "name": "BASIC_USER"
        }
      ]
    }
  },
  "data": {
    "success": true,
    "user": {
      "uuid": "01170b7c-6b5c-480d-b3dd-2a619195fc56",
      "email": "[email protected]"
    },
    "subscription": {
      "uuid": "e7f7e5db-82ec-4a1f-a827-62191fc7d564",
      "planName": "PRO_PLAN",
      "endsAt": 8640000000000000,
      "createdAt": 0,
      "updatedAt": 0,
      "cancelled": 0,
      "subscriptionId": 1
    }
  }
}

Apart from the userUuid, each 'role' has a uuid (I assume on a per-user basis) and each subscription has a uuid, I assume a per user one, as it's only the name that is being checked.

I think BASIC_USER is the same as a 'free' user, so basically every user at least has this tier.

A user who has only the BASIC_USER tier returns data without a subscription (only success and user in that case).

It also tries to access v1/users/{uuid}/subscription, which returns:

subscriptions getaddrinfo EAI_AGAIN payments

The latter one seems like something only used on the hosted instance for payment info, which is not applicable to self-hosted users. The functions responsible for these requests can be found here.

So to implement it, it would take to:

  • Either have a database table for per-user roles (and a table for plans) or a environment variable to set the PLAN_STATUS
  • In case of the former, have a way for a user to set it
  • Implement an endpoint for v2/subscriptions, which returns the required info
  • Optionally implement a dummy endpoint for v1/users/{uuid}/subscription

I think the environment variable is the most easy option, while the custom subscription enabler (maybe with a reference that kindly asks to donate something to Standard Notes) being the most 'ethical' way.

SerialDestructor avatar Apr 06 '22 08:04 SerialDestructor

Very interesting, thanks for looking into this @SerialDestructor !

My Standard File server is running behind a caddy reverse proxy, and it could be set up to serve static content like a JSON file at v2/subscriptions if necessary. However, it seems that I would need to know the UUID of every user and also I'm concerned about exposing the username since it's part of the answer in plain text. Or is this encrypted somehow?

Can any of the options you enumerate be implemented by end-users like me without adding code to this project? I wish I knew how to code :-)

valantur avatar Apr 06 '22 21:04 valantur

However, it seems that I would need to know the UUID of every user and also I'm concerned about exposing the username since it's part of the answer in plain text. Or is this encrypted somehow?

No, the response is different for each user account. With a static JSON file, the email address of the notes account is there in plain text, for everyone to see who knows the endpoint.

Can any of the options you enumerate be implemented by end-users like me without adding code to this project? I wish I knew how to code :-)

Well, as you mentioned, it would be possible to do it hard coded for one account and it could work, but again, your account email address and uuid will be exposed. It would stay away from that option.

I have coding expierence, but not in GoLang. I could give it a try nevertheless, but there are a lot of project-specific things (such as restricted vs non-restricted endpoints, which both seem only accessible after a login) which I don't know the difference about. Some insights about where to look would certainly help.

SerialDestructor avatar Apr 06 '22 21:04 SerialDestructor

"restricted" routes need authentication (Authorization header in request).


I tried to dig in this feature, using the standalone docker-compose and app.standardnotes.org:

  • Retrieve user's subscription

    GET /v1/users/dd07dcba-3884-4dbd-9cf4-960fbb57bd99/subscription
    
    200 OK
    {
        "meta": {
            "auth": {
                "userUuid": "dd07dcba-3884-4dbd-9cf4-960fbb57bd99",
                "roles": [
                    {
                        "uuid": "8047edbb-a10a-4ff8-8d53-c2cae600a8e8",
                        "name": "PRO_USER"
                    },
                    {
                        "uuid": "8802d6a3-b97c-4b25-968a-8fb21c65c3a1",
                        "name": "BASIC_USER"
                    }
                ]
            }
        },
        "data": {
            "success": true,
            "user": {
                "uuid": "dd07dcba-3884-4dbd-9cf4-960fbb57bd99",
                "email": "[email protected]"
            },
            "subscription": {
                "uuid": "caf81bfc-bfcf-11ec-b43b-0242ac120008",
                "planName": "PRO_PLAN",
                "endsAt": 8640000000000000,
                "createdAt": 0,
                "updatedAt": 0,
                "cancelled": 0,
                "subscriptionId": 1
            }
        }
    }
    
  • Check payment? There is no Authorization header from the request so the route seems not authenticated

    GET /v2/subscriptions
    
    500 Internal Server Error
    getaddrinfo ENOTFOUND payments
    
  • Load the user's features

    GET /v1/users/dd07dcba-3884-4dbd-9cf4-960fbb57bd99/features
    
    200 OK
    { Large JSON payload of all features and their URLs }
    

I tried to fake GET /v1/users/:id/subscription with data.user.email/data.user.uuid/meta.auth.userUuid updated and GET /v2/subscriptions with the same 500 Internal Server Error payload but it does not work, GET /v1/users/:id/features is never requested. I don't know what I'm missing here.

mdouchement avatar Apr 19 '22 13:04 mdouchement

I was already experimenting with this, but since I don't know Go to well and needed to do some database changes (not familiar with BoltDB either), it was taking up too much time for the time being.

@mdouchement could you share what you tried? I will see if I can look into that as soon as I have some time.

SerialDestructor avatar Apr 21 '22 13:04 SerialDestructor

Yeah sure, here my sandbox https://github.com/mdouchement/standardfile/compare/subscription-sandbox?expand=1

mdouchement avatar Apr 21 '22 13:04 mdouchement

I can't pretend I haven't been checking this thread daily, hoping for an update from you :-)

valantur avatar Jun 16 '22 16:06 valantur

Maybe this will help understand how it works? https://github.com/standardnotes/standalone/issues/64#issuecomment-1064719310

BobWs avatar Jun 17 '22 05:06 BobWs

With standardnotes you only have to add these to the database to unlock the pro feature.


INSERT INTO user_roles (role_uuid , user_uuid) VALUES ( ( select uuid from roles where name="PRO_USER" order by version desc limit 1 ) ,( select uuid from users where email="<EMAIL@ADDR>" )  ) ON DUPLICATE KEY UPDATE role_uuid = VALUES(`role_uuid`);

insert into user_subscriptions set uuid = UUID() , plan_name="PRO_PLAN" , ends_at = 8640000000000000, created_at = 0 , updated_at = 0,user_uuid= (select uuid from users where email="<EMAIL@ADDR>") , subscription_id=1;

BobWs avatar Aug 31 '22 06:08 BobWs