88 |
89 |
--------------------------------------------------------------------------------
/content/1.getting-started/0.overview.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Welcome to Entitled!
3 | ---
4 |
5 | Entitled is a Python library that was designed with one core goal in mind : to provide a framework to easily organize, centralize and enforce their authorization logic.
6 |
7 | This library is written in pure Python. While it was initially designed with FastAPI in mind, it can be used in any context.
8 |
9 | Rules in Entitled are written as simple Python yourself. As such they can support any kind of authorization model you wish to implement, be it old-school ACL, RBAC, or others.
10 |
11 | You focus on building the logic however you want, Entitled takes care of the rest.
12 |
--------------------------------------------------------------------------------
/content/1.getting-started/1.quickstart.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Getting started with Entitled.
3 | ---
4 |
5 |
6 | ## Installation
7 |
8 | Entitled can be installed with Pip, Poetry or uv. Other tools have not been tested.
9 |
10 | Entitled also requires Python >= 3.12.
11 |
12 | ```shell
13 | pip install entitled
14 | poetry install entitled
15 | uv add entitled
16 | ```
17 |
18 | ## Basic usage
19 |
20 | Entitled revolves around two core elements. :
21 |
22 | - `Rules` are Python functions implementing your authorization logic.
23 | - `Policies` are sets of `Rules` relating to the same type of resource.
24 |
25 | You can declare a new `Policy` on the `Post` type like so:
26 |
27 | ```py
28 | import entitled
29 |
30 | post_policy = entitled.Policy[Post]()
31 | ```
32 |
33 | A `Policy` provides a decorator to easily register rules. You only need to tack it onto a Python function implementing some authorization logic. You will also need to provide it with a name.
34 |
35 | ```py
36 | @post_policy.rule
37 | def can_edit_post(actor, resource: Post, context) -> bool:
38 | return resource.owner == actor
39 | ```
40 |
41 | ### > [!WARNING]Rule specification
42 |
43 | To be a valid rule, a function must follow a specific signature defined by Entitled. It must be:
44 |
45 | - An async callable
46 | - Accept an 'actor' positional parameter
47 | - Return a boolean or a `Response`` object
48 |
49 | Rules may define any number of additional parameters.
50 |
51 | ## Making authorization decisions
52 |
53 | ### The Client
54 |
55 | While you can *technically* call Policies directly, you will want to use an instance of `Client` class.
56 |
57 | ```py
58 | import entitled
59 |
60 | auth_client = entitled.Client()
61 | ```
62 |
63 | This client will serve as your single source of truth for all your authorization decisions. Once it is declared, you can register policies on it using the `register()` method.
64 |
65 | ```python
66 | auth_client.register(post_policy)
67 | ```
68 |
69 | ::callout{icon="i-heroicons-exclamation-triangle" color="amber"}
70 | Manually registering policies may sound cumbersome... and rightfully so. Entitled provides a mechanism to auto-discover and register policies. More on that later.
71 | ::
72 |
73 | #### Making decisions
74 |
75 | Once your policies are registered, making authorization decisions is extremely simple! The `Client` object provides the `allows` function to evaluate a rule. This function takes as parameters the name of the rule you want to authorize, an actor, a resource and optionally a context dictionary.
76 |
77 | ```py
78 | if auth_client.allows("edit", actor, resource):
79 | # Your application logic...
80 |
81 | ```
82 |
83 | The `allows` functions returns a boolean indicating whether the actor can perform the given action.
84 |
85 | If you want, you can also make use of the `authorize` methods that will automatically raise an `AuthorizationException` if the user is not allowed to perform the action. Both function are called the exact same way.
86 |
87 | ## Wrapping things up
88 |
89 | That's it for the absolute basics of Entitled! But we barely scratched the surface here, as the library offers many more features, including but not limited to :
90 |
91 | - Resource-bound policies, or how to group your rules around a specific resource type.
92 | - Auto-discovery of policies.
93 | - Answering the question "*What* can this actor do?"
94 |
95 | Check out the detailed documentation!
96 |
--------------------------------------------------------------------------------
/content/1.getting-started/_dir.yml:
--------------------------------------------------------------------------------
1 | title: 'Getting Started'
2 | icon: ph:star-duotone
3 | navigation.redirect: /getting-started/overview
4 |
--------------------------------------------------------------------------------
/content/2.basics/1.rules.md:
--------------------------------------------------------------------------------
1 | Rules are the basic building blocks of Entitled. Fundamentally, a `Rule` is a
2 | simple wrapper around a (somewhat specific) Python function.
3 |
4 | Semantically, Rules describes "facts" about your application and about the relationships
5 | between the various actors and resources of your app.
6 |
7 | If you have ever used Laravel's authorization API, this API is very much inspired by it.
8 |
9 | ## Writing Rules
10 |
11 | To make a new rule, you just need a Python `Callable` that satisfies three criteria:
12 |
13 | - It must be an async Callable
14 | - It should accept at least one positional parameter for the actor
15 | - It should return either a boolean or a Response object.
16 |
17 | These are the minimum requirements to create a rule. Additionally, if the `Callable` used
18 | is anonymous, you will need to provide a name for the rule:
19 |
20 | ```py
21 | # With a lambda
22 | is_admin = Rule("is_admin", lambda actor: has_admin_status(actor))
23 |
24 | # With a declared callable
25 | async def is_admin(actor: Actor):
26 | return has_admin_status(actor)
27 |
28 | rule_is_admin = Rule(is_admin)
29 | ```
30 |
31 | Note : once we tackle policies, you'll be provided a third way to create a rule through policies.
32 |
33 | ## Using Rules
34 |
35 | Rules are `Callable` object themselves that defer to the underlying function, so using them may be ass simple as calling them on the relevant objects
36 |
37 | ```py
38 | if is_admin(user, post):
39 | print("User authorized")
40 | ```
41 |
42 | However, the `Rule` class also provides several utility functions :
43 |
44 | - `Rule.inspect` will always wrap the result into a `Response` object, supplying a default error message.
45 | - `Rule.allows` does the opposite and systematically returns a boolean (it is equivalent to using `rule.inspect().allowed()`)
46 | - `Rule.denies` is equivalent to `not rule.allows()`
47 | - `Rule.authorize` evaluates the rule, but returns an exception in case if the evaluation would return an error. The exception essentially calls `inspect` and wraps any error message into an `AuthorizationException`.
48 |
49 | ## A Warning
50 |
51 | Rules are meant to be extremely flexible in their definition, but you'll notice that the evaluation provided by the `Rule` class don't retain the signature from the underlying Callable
52 |
53 | In most cases, you may pass any parameters to an evaluation function, and Entitled will match the parameters to the signature of the underlying function, thus preventing any errors for extra parameters. This system is still quite unoptimized : there is one massive footgun that we'll discover when discussing the `Client`.
54 |
--------------------------------------------------------------------------------
/content/2.basics/2.policies.md:
--------------------------------------------------------------------------------
1 | Policies provide a way to group your authorization logic around a particular resource.
2 |
3 | A policy is essentially a registry of rules relating to the same resource type.
4 |
5 | ## Declaring a Policy
6 |
7 | The policy constructor does not take any parameter, but take a type argument that will define which resource this Policy refers to.
8 |
9 | That's it! Once you have a policy created, you may start registering rules.
10 |
11 | ```py
12 | post_policy = Policy[Post]()
13 | ```
14 |
15 | ## Adding Rules to your Policy
16 |
17 | To add a rule to your policy, you have two options:
18 |
19 | You may first take a previously defined `Rule` and call the `register` method on your policy. This method calls for
20 | a name and a Rule object:
21 |
22 | ```py
23 | async def can_edit(actor: User, post: Post):
24 | return post.owner == actor
25 |
26 | post_policy = Policy[Post]()
27 | post_policy.register("update", can_edit)
28 |
29 | ```
30 |
31 | The other, and arguably more practical option, is to used the `rule` decorator provided by your new policy:
32 |
33 | ```py
34 | post_policy = Policy[Post]()
35 |
36 | @policy.rule
37 | async def update(actor: User, post: Post):
38 | return post.owner == actor
39 | ```
40 |
41 | This will automatically take your function, create a new rule from it, then register it on your policy.
42 |
43 | ### A warning
44 |
45 | Be aware that registering two rules with the same name or action name will simply overwrite the former rule defined for this name:
46 |
47 | ```py
48 | async def can_edit(actor: User, post: Post):
49 | return post.owner == actor
50 | async def can_read(actor: User, post: Post):
51 | return post.owner == actor || actor in post.guests
52 |
53 | post_policy.register("update", can_edit)
54 | post_policy.register("update", can_read) # Overwrites the previously defined rule
55 | ```
56 |
57 | ## Using a policy
58 |
59 | The `Policy` class exposes the same evaluation functions as the `Rule` class does, but requires that you pass in an action name as well. Here are few examples:
60 |
61 | ```py
62 | if post_policy.allows('update', actor, post):
63 | # The action is authorized...
64 | else:
65 | # The action is forbidden
66 | ```
67 |
68 | ```py
69 | response = post_policy.inspect("update", actor, post)
70 | match response:
71 | case Ok(_):
72 | # The action is authorized
73 | case Err(msg):
74 | # The action is forbidden
75 | ```
76 |
77 | ```py
78 | post_policy.authorize("update", actor, post)
79 | # The action is authorized
80 | ```
81 |
82 | On top of these calls, a policy also exposes a `grants` call. This one is a bit different as it does not take an action name as parameter, but instead will evaluate all rules in the policy for the given set of parameters provided to the function and return the result as a dictionary.
83 |
84 | For example:
85 |
86 | ```py
87 | policy = Policy[Post]()
88 | @policy.rule
89 | async def create(actor: User):
90 | return True
91 |
92 | @policy.rule
93 | async def view(actor: User, post: Post):
94 | return post.owner == user | user in post.access_list
95 |
96 | @policy.rule
97 | async def update(actor: User, post: Post):
98 | return post.owner == actor
99 |
100 | print(policy.grants(post_owner, post))
101 | # {
102 | # "create": True,
103 | # "view": True,
104 | # "update": True,
105 | # }
106 | print(policy.grants(post_reader, post))
107 | # {
108 | # "create": True,
109 | # "view": True,
110 | # "update": False,
111 | # }
112 | ```
113 |
--------------------------------------------------------------------------------
/content/2.basics/3.client.md:
--------------------------------------------------------------------------------
1 | The `Client` object is designed to act as the source of truth and decision-making point of your application.
2 |
3 | As a general rule, you should endeavor to use that Client object to make any authorization decision, not
4 | going through specific policies or rules.
5 |
6 | ## Accessing the Client
7 |
8 | The `Client` class takes a single optional parameters : a path to the folder containing your policies files.
9 |
10 | You may place all of your policies in a single file or split them into multiple files in the same folder, it doesn't matter. Upon initialization, if the `path` parameter is provided, the `Client` will inspect that folder and register any `Policy` instance it finds.
11 |
12 | You may also manually load policies after the initialization of the client, by calling `load_policies()` on it with a path parameter.
13 |
14 | ## Using the Client
15 |
16 | The `Client` class exposes the same evaluation functions as the `Policy` class, with one little twist:
17 | These function must now receive a `resource` positional argument that may be an instance of a class, or the class itself.
18 |
19 | This argument will first be used by the client to determine which policy should be used for this resource, then will
20 | be passed down all the way to the actual rule evaluation.
21 | As such, passing a class when the rule would expect an instance will cause Entitled to correctly determine which rule should be evaluated, but the evaluation itself will fail:
22 |
23 | ```py
24 | policy = Policy[Post]()
25 |
26 | @rule.policy
27 | async def view(actor: Actor, post: Post):
28 | ...
29 |
30 | client = Client()
31 | await client.authorize(actor, Post) # The client will resolve this to the proper policy, but the evaluation itself will fail
32 |
33 | ```
34 |
35 | Aside from this, the `authorize`, `allows`, `denies`, `inspect` and `grants` functions work like their counterpart on `Policy` and in fact defer to these functions under the hood.
36 |
37 | ## [!WARNING] A potential (and temporary) foot-gun with Client
38 |
39 | As we discussed in the "rules" section, the flexibility allowed when defining `Rules` and when calling evaluation function is still a bit unrefined and may come with a few potholes.
40 |
41 | In particular, one such pothole that arises from the current implementation is that, when defining rules that don't make use of the 'resource' field but require additional fields, `Client` functions will pass the resource object as an argument to the rule evaluation, and if no precaution is taken, will pass it down as the additional parameter. Consider this:
42 |
43 | ```py
44 | policy = Policy[Post]()
45 |
46 | @policy.rule
47 | async def update(user: User, context: dict[str, Any]):
48 | return "some_user_list" in context and user in context["some_user_list"]:
49 |
50 | client.authorize("update", post, context) # current implementation raises an error in this case.
51 | ```
52 |
53 | A possible workaround in this case to define such rules like this:
54 |
55 | ```py
56 | @policy.rule
57 | async def update(user: User, *, context: dict[str, Any]):
58 | return "some_user_list" in context and user in context["some_user_list"]:
59 | ```
60 |
61 | This will ensure that the rule only accepts `context` as a keyword argument. You may then call you method like this:
62 |
63 | ```py
64 | client.authorize("update", post, context=context) # This will evaluate correctly
65 | ```
66 |
--------------------------------------------------------------------------------
/content/2.basics/_dir.yml:
--------------------------------------------------------------------------------
1 | title: 'Basics'
2 | icon: heroicons-outline:bookmark-alt
3 | navigation.redirect: /basics/core-concepts
4 |
--------------------------------------------------------------------------------
/content/index.yml:
--------------------------------------------------------------------------------
1 | title: "Entitled - A library for authorization enforcement"
2 | description: A fairly simple library with one goal - make enforcing your authorization logic as easy as possible, letting the developpers focus on their model and logic.
3 | navigation: false
4 | hero:
5 | title: "A library for authorization enforcement."
6 | description: "Helps you organize and enforce authorization so you can focus on the decision-making logic."
7 | orientation: horizontal
8 | links:
9 | - label: Get started
10 | icon: i-heroicons-arrow-right-20-solid
11 | trailing: true
12 | to: "/getting-started/overview"
13 | size: lg
14 | - label: Source Code
15 | icon: i-simple-icons-github
16 | size: lg
17 | color: gray
18 | to: https://github.com/xefi/python-entitled
19 | target: _blank
20 | code: |
21 | ```bash [Terminal]
22 | pip install entitled
23 |
24 | # with poetry
25 | poetry install entitled
26 | ```
27 | features:
28 | title: "Key features"
29 | items:
30 | - title: "Model-agnostic"
31 | icon: "i-mdi-cable-data"
32 | description: "RBAC, ABAC, ACL... you choose the model, Entitled enforces it."
33 | - title: "Easy organization"
34 | icon: "i-mdi-cable-data"
35 | description: "Policies and Rules neatly organize your decision logic."
36 | - title: "Single decision point"
37 | icon: "i-mdi-cable-data"
38 | description: "A single entrypoint for your authorization decisions."
39 | basic_usages:
40 | - title: "Define your resource and actors, on your terms."
41 | code: |
42 | ```python
43 | #resources.py
44 |
45 | class User:
46 | id: int
47 |
48 | class Post:
49 | id: int
50 | owner: User
51 | ```
52 | - title: "Create a policy for your resource."
53 | code: |
54 | ```python
55 | # policies.py
56 |
57 | import entitled
58 |
59 | post_policy = entitled.Policy[Post]()
60 | ```
61 | - title: "Rules encapsulate your logic."
62 | code: |
63 | ```python
64 | # policies.py
65 |
66 | @post_policy.rule
67 | async def can_edit_post(actor: User, resource: Post) -> bool:
68 | return resource.owner == actor
69 | ```
70 | - title: "Enforce your policies."
71 | code: |
72 | ```python
73 | client = entitled.Client()
74 | client.register(post_policy)
75 |
76 | if await client.allows("edit", user, post):
77 | # your business logic
78 | ```
79 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import withNuxt from './.nuxt/eslint.config.mjs'
3 |
4 | export default withNuxt(
5 | // Your custom configs here
6 | )
7 |
--------------------------------------------------------------------------------
/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | // https://nuxt.com/docs/api/configuration/nuxt-config
2 | export default defineNuxtConfig({
3 | extends: ['@nuxt/ui-pro'],
4 |
5 | modules: [
6 | '@nuxt/content',
7 | '@nuxt/eslint',
8 | '@nuxt/fonts',
9 | '@nuxt/image',
10 | '@nuxt/ui',
11 | '@nuxthq/studio',
12 | 'nuxt-og-image'
13 | ],
14 |
15 | hooks: {
16 | // Define `@nuxt/ui` components as global to use them in `.md` (feel free to add those you need)
17 | 'components:extend': (components) => {
18 | const globals = components.filter(c => ['UButton', 'UIcon'].includes(c.pascalName))
19 |
20 | globals.forEach(c => c.global = true)
21 | }
22 | },
23 |
24 | colorMode: {
25 | disableTransition: true
26 | },
27 |
28 | nitro: {
29 | prerender: {
30 | routes: [
31 | '/'
32 | ],
33 | crawlLinks: true
34 | }
35 | },
36 |
37 | content: {
38 | highlight: {
39 | langs: ['python']
40 | }
41 | },
42 |
43 | routeRules: {
44 | '/api/search.json': { prerender: true }
45 | },
46 |
47 | devtools: {
48 | enabled: true
49 | },
50 |
51 | typescript: {
52 | strict: false
53 | },
54 |
55 | future: {
56 | compatibilityVersion: 4
57 | },
58 |
59 | eslint: {
60 | config: {
61 | stylistic: {
62 | commaDangle: 'never',
63 | braceStyle: '1tbs'
64 | }
65 | }
66 | },
67 |
68 | compatibilityDate: '2024-07-11'
69 | })
70 |
--------------------------------------------------------------------------------
/nuxt.schema.ts:
--------------------------------------------------------------------------------
1 | import { field, group } from '@nuxthq/studio/theme'
2 |
3 | export default defineNuxtSchema({
4 | appConfig: {
5 | ui: group({
6 | title: 'UI',
7 | description: 'UI Customization.',
8 | icon: 'i-mdi-palette-outline',
9 | fields: {
10 | icons: group({
11 | title: 'Icons',
12 | description: 'Manage icons used in UI Pro.',
13 | icon: 'i-mdi-application-settings-outline',
14 | fields: {
15 | search: field({
16 | type: 'icon',
17 | title: 'Search Bar',
18 | description: 'Icon to display in the search bar.',
19 | icon: 'i-mdi-magnify',
20 | default: 'i-heroicons-magnifying-glass-20-solid'
21 | }),
22 | dark: field({
23 | type: 'icon',
24 | title: 'Dark mode',
25 | description: 'Icon of color mode button for dark mode.',
26 | icon: 'i-mdi-moon-waning-crescent',
27 | default: 'i-heroicons-moon-20-solid'
28 | }),
29 | light: field({
30 | type: 'icon',
31 | title: 'Light mode',
32 | description: 'Icon of color mode button for light mode.',
33 | icon: 'i-mdi-white-balance-sunny',
34 | default: 'i-heroicons-sun-20-solid'
35 | }),
36 | external: field({
37 | type: 'icon',
38 | title: 'External Link',
39 | description: 'Icon for external link.',
40 | icon: 'i-mdi-arrow-top-right',
41 | default: 'i-heroicons-arrow-up-right-20-solid'
42 | }),
43 | chevron: field({
44 | type: 'icon',
45 | title: 'Chevron',
46 | description: 'Icon for chevron.',
47 | icon: 'i-mdi-chevron-down',
48 | default: 'i-heroicons-chevron-down-20-solid'
49 | }),
50 | hash: field({
51 | type: 'icon',
52 | title: 'Hash',
53 | description: 'Icon for hash anchors.',
54 | icon: 'i-ph-hash',
55 | default: 'i-heroicons-hashtag-20-solid'
56 | })
57 | }
58 | }),
59 | primary: field({
60 | type: 'string',
61 | title: 'Primary',
62 | description: 'Primary color of your UI.',
63 | icon: 'i-mdi-palette-outline',
64 | default: 'green',
65 | required: ['sky', 'mint', 'rose', 'amber', 'violet', 'emerald', 'fuchsia', 'indigo', 'lime', 'orange', 'pink', 'purple', 'red', 'teal', 'yellow', 'green', 'blue', 'cyan', 'gray', 'white', 'black']
66 | }),
67 | gray: field({
68 | type: 'string',
69 | title: 'Gray',
70 | description: 'Gray color of your UI.',
71 | icon: 'i-mdi-palette-outline',
72 | default: 'slate',
73 | required: ['slate', 'cool', 'zinc', 'neutral', 'stone']
74 | })
75 | }
76 | }),
77 | seo: group({
78 | title: 'SEO',
79 | description: 'SEO configuration.',
80 | icon: 'i-ph-app-window',
81 | fields: {
82 | siteName: field({
83 | type: 'string',
84 | title: 'Site Name',
85 | description: 'Name used in ogSiteName and used as second part of your page title (My page title - Nuxt UI Pro).',
86 | icon: 'i-mdi-web',
87 | default: []
88 | })
89 | }
90 | }),
91 | header: group({
92 | title: 'Header',
93 | description: 'Header configuration.',
94 | icon: 'i-mdi-page-layout-header',
95 | fields: {
96 | logo: group({
97 | title: 'Logo',
98 | description: 'Header logo configuration.',
99 | icon: 'i-mdi-image-filter-center-focus-strong-outline',
100 | fields: {
101 | light: field({
102 | type: 'media',
103 | title: 'Light Mode Logo',
104 | description: 'Pick an image from your gallery.',
105 | icon: 'i-mdi-white-balance-sunny',
106 | default: ''
107 | }),
108 | dark: field({
109 | type: 'media',
110 | title: 'Dark Mode Logo',
111 | description: 'Pick an image from your gallery.',
112 | icon: 'i-mdi-moon-waning-crescent',
113 | default: ''
114 | }),
115 | alt: field({
116 | type: 'string',
117 | title: 'Alt',
118 | description: 'Alt to display for accessibility.',
119 | icon: 'i-mdi-alphabet-latin',
120 | default: ''
121 | })
122 | }
123 | }),
124 | search: field({
125 | type: 'boolean',
126 | title: 'Search Bar',
127 | description: 'Hide or display the search bar.',
128 | icon: 'i-mdi-magnify',
129 | default: true
130 | }),
131 | colorMode: field({
132 | type: 'boolean',
133 | title: 'Color Mode',
134 | description: 'Hide or display the color mode button in your header.',
135 | icon: 'i-mdi-moon-waning-crescent',
136 | default: true
137 | }),
138 | links: field({
139 | type: 'array',
140 | title: 'Links',
141 | description: 'Array of link object displayed in header.',
142 | icon: 'i-mdi-link-variant',
143 | default: []
144 | })
145 | }
146 | }),
147 | footer: group({
148 | title: 'Footer',
149 | description: 'Footer configuration.',
150 | icon: 'i-mdi-page-layout-footer',
151 | fields: {
152 | credits: field({
153 | type: 'string',
154 | title: 'Footer credits section',
155 | description: 'Text to display as credits in the footer.',
156 | icon: 'i-mdi-circle-edit-outline',
157 | default: ''
158 | }),
159 | colorMode: field({
160 | type: 'boolean',
161 | title: 'Color Mode',
162 | description: 'Hide or display the color mode button in the footer.',
163 | icon: 'i-mdi-moon-waning-crescent',
164 | default: false
165 | }),
166 | links: field({
167 | type: 'array',
168 | title: 'Links',
169 | description: 'Array of link object displayed in footer.',
170 | icon: 'i-mdi-link-variant',
171 | default: []
172 | })
173 | }
174 | }),
175 | toc: group({
176 | title: 'Table of contents',
177 | description: 'TOC configuration.',
178 | icon: 'i-mdi-table-of-contents',
179 | fields: {
180 | title: field({
181 | type: 'string',
182 | title: 'Title',
183 | description: 'Text to display as title of the main toc.',
184 | icon: 'i-mdi-format-title',
185 | default: ''
186 | }),
187 | bottom: group({
188 | title: 'Bottom',
189 | description: 'Bottom TOC configuration.',
190 | icon: 'i-mdi-table-of-contents',
191 | fields: {
192 | title: field({
193 | type: 'string',
194 | title: 'Title',
195 | description: 'Text to display as title of the bottom toc.',
196 | icon: 'i-mdi-format-title',
197 | default: ''
198 | }),
199 | edit: field({
200 | type: 'string',
201 | title: 'Edit Page Link',
202 | description: 'URL of your repository content folder.',
203 | icon: 'i-ph-note-pencil',
204 | default: ''
205 | }),
206 | links: field({
207 | type: 'array',
208 | title: 'Links',
209 | description: 'Array of link object displayed in bottom toc.',
210 | icon: 'i-mdi-link-variant',
211 | default: []
212 | })
213 | }
214 | })
215 | }
216 | })
217 | }
218 | })
219 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "python-entitled-doc",
3 | "version": "0.2.0",
4 | "private": true,
5 | "type": "module",
6 | "scripts": {
7 | "build": "nuxt build",
8 | "dev": "nuxt dev",
9 | "generate": "nuxt generate",
10 | "preview": "nuxt preview",
11 | "postinstall": "nuxt prepare",
12 | "lint": "eslint .",
13 | "typecheck": "nuxt typecheck"
14 | },
15 | "dependencies": {
16 | "@iconify-json/heroicons": "^1.2.0",
17 | "@iconify-json/simple-icons": "^1.2.4",
18 | "@nuxt/content": "^2.13.2",
19 | "@nuxt/fonts": "^0.9.2",
20 | "@nuxt/image": "^1.8.0",
21 | "@nuxt/ui-pro": "^1.4.3",
22 | "nuxt": "^3.13.2",
23 | "nuxt-og-image": "^3.0.4"
24 | },
25 | "devDependencies": {
26 | "@nuxt/eslint": "^0.5.7",
27 | "@nuxthq/studio": "^2.1.1",
28 | "eslint": "^9.11.1",
29 | "vue-tsc": "^2.1.6"
30 | },
31 | "resolutions": {
32 | "@nuxtjs/tailwindcss": "nightly"
33 | },
34 | "packageManager": "pnpm@9.11.0"
35 | }
36 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xefi/python-entitled-doc/fa1b459ba214f9196f7c0bc1d7b73397275239c0/public/favicon.ico
--------------------------------------------------------------------------------
/public/social-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xefi/python-entitled-doc/fa1b459ba214f9196f7c0bc1d7b73397275239c0/public/social-card.png
--------------------------------------------------------------------------------
/public/xefi-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
21 |
--------------------------------------------------------------------------------
/public/xefi-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
21 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "github>nuxt/renovate-config-nuxt"
4 | ],
5 | "lockFileMaintenance": {
6 | "enabled": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/server/api/search.json.get.ts:
--------------------------------------------------------------------------------
1 | import { serverQueryContent } from '#content/server'
2 |
3 | export default eventHandler(async (event) => {
4 | return serverQueryContent(event).where({ _type: 'markdown', navigation: { $ne: false } }).find()
5 | })
6 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../.nuxt/tsconfig.server.json"
3 | }
4 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 | import defaultTheme from 'tailwindcss/defaultTheme'
3 |
4 | export default >{
5 | theme: {
6 | extend: {
7 | fontFamily: {
8 | sans: ['DM Sans', ...defaultTheme.fontFamily.sans]
9 | },
10 | colors: {
11 | green: {
12 | 50: '#EFFDF5',
13 | 100: '#D9FBE8',
14 | 200: '#B3F5D1',
15 | 300: '#75EDAE',
16 | 400: '#00DC82',
17 | 500: '#00C16A',
18 | 600: '#00A155',
19 | 700: '#007F45',
20 | 800: '#016538',
21 | 900: '#0A5331',
22 | 950: '#052e16'
23 | }
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // https://nuxt.com/docs/guide/concepts/typescript
3 | "extends": "./.nuxt/tsconfig.json"
4 | }
5 |
--------------------------------------------------------------------------------