mirror of
https://github.com/YunoHost/apps.git
synced 2024-09-03 20:06:07 +02:00
Initial commit for new app store
This commit is contained in:
parent
53027ff76a
commit
a057aab198
8 changed files with 511 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -15,3 +15,4 @@ tools/autopatches/token
|
|||
|
||||
__pycache__
|
||||
app_list_auto_update.log
|
||||
venv
|
||||
|
|
121
store/app.py
Normal file
121
store/app.py
Normal file
|
@ -0,0 +1,121 @@
|
|||
from flask import Flask, send_from_directory, render_template, session, redirect, request
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import random
|
||||
import urllib
|
||||
import json
|
||||
from settings import DISCOURSE_SSO_SECRET, DISCOURSE_SSO_ENDPOINT, CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE
|
||||
app = Flask(__name__)
|
||||
|
||||
app.debug = True
|
||||
app.config["DEBUG"] = True
|
||||
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
||||
|
||||
catalog = json.load(open("apps.json"))
|
||||
catalog['categories'] = {c['id']:c for c in catalog['categories']}
|
||||
|
||||
category_color = {
|
||||
"synchronization": "sky",
|
||||
"publishing": "yellow",
|
||||
"communication": "amber",
|
||||
"office": "lime",
|
||||
"productivity_and_management": "purple",
|
||||
"small_utilities": "",
|
||||
"reading": "emerald",
|
||||
"multimedia": "fuchsia",
|
||||
"social_media": "rose",
|
||||
"games": "violet",
|
||||
"dev": "stone",
|
||||
"system_tools": "white",
|
||||
"iot": "orange",
|
||||
"wat": "teal",
|
||||
}
|
||||
|
||||
for id_, category in catalog['categories'].items():
|
||||
category["color"] = category_color[id_]
|
||||
|
||||
wishlist = json.load(open("wishlist.json"))
|
||||
|
||||
# This is the secret key used for session signing
|
||||
app.secret_key = ''.join([str(random.randint(0, 9)) for i in range(99)])
|
||||
|
||||
|
||||
@app.route('/login_using_discourse')
|
||||
def login_using_discourse():
|
||||
"""
|
||||
Send auth request to Discourse:
|
||||
"""
|
||||
|
||||
nonce, url = create_nonce_and_build_url_to_login_on_discourse_sso()
|
||||
|
||||
session.clear()
|
||||
session["nonce"] = nonce
|
||||
|
||||
return redirect(url)
|
||||
|
||||
|
||||
@app.route('/sso_login_callback')
|
||||
def sso_login_callback():
|
||||
response = base64.b64decode(request.args['sso'].encode()).decode()
|
||||
user_data = urllib.parse.parse_qs(response)
|
||||
if user_data['nonce'][0] != session.get("nonce"):
|
||||
return "Invalid nonce", 401
|
||||
else:
|
||||
session.clear()
|
||||
session['user'] = {
|
||||
"id": user_data["external_id"][0],
|
||||
"username": user_data["username"][0],
|
||||
"avatar_url": user_data["avatar_url"][0],
|
||||
}
|
||||
return redirect("/")
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect("/")
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template("index.html", user=session.get('user', {}), catalog=catalog)
|
||||
|
||||
|
||||
@app.route('/catalog')
|
||||
def browse_catalog(category_filter=None):
|
||||
return render_template("catalog.html", user=session.get('user', {}), catalog=catalog)
|
||||
|
||||
|
||||
@app.route('/app/<app_id>')
|
||||
def app_info(app_id):
|
||||
infos = catalog["apps"].get(app_id)
|
||||
if not infos:
|
||||
return f"App {app_id} not found", 404
|
||||
return render_template("app.html", user=session.get('user', {}), app_id=app_id, infos=infos)
|
||||
|
||||
|
||||
@app.route('/wishlist')
|
||||
def browse_wishlist():
|
||||
return render_template("wishlist.html", user=session.get('user', {}), wishlist=wishlist)
|
||||
|
||||
|
||||
|
||||
################################################
|
||||
|
||||
def create_nonce_and_build_url_to_login_on_discourse_sso():
|
||||
"""
|
||||
Redirect the user to DISCOURSE_ROOT_URL/session/sso_provider?sso=URL_ENCODED_PAYLOAD&sig=HEX_SIGNATURE
|
||||
"""
|
||||
|
||||
nonce = ''.join([str(random.randint(0, 9)) for i in range(99)])
|
||||
|
||||
url_data = {"nonce": nonce, "return_sso_url": CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE}
|
||||
url_encoded = urllib.parse.urlencode(url_data)
|
||||
payload = base64.b64encode(url_encoded.encode()).decode()
|
||||
sig = hmac.new(DISCOURSE_SSO_SECRET.encode(), msg=payload.encode(), digestmod=hashlib.sha256).hexdigest()
|
||||
data = {"sig": sig, "sso": payload}
|
||||
url = f"{DISCOURSE_SSO_ENDPOINT}?{urllib.parse.urlencode(data)}"
|
||||
|
||||
return nonce, url
|
1
store/requirements.txt
Normal file
1
store/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Flask==2.3.2
|
9
store/templates/app.html
Normal file
9
store/templates/app.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<div class="max-w-screen-lg mx-auto pt-5">
|
||||
|
||||
<h2>{{ app_id }}</h2>
|
||||
|
||||
<p>{{ infos }}</p>
|
||||
</div>
|
||||
{% endblock %}
|
123
store/templates/base.html
Normal file
123
store/templates/base.html
Normal file
|
@ -0,0 +1,123 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>YunoHost app store</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.2.0/css/fork-awesome.min.css" integrity="sha256-XoaMnoYC5TH6/+ihMEnospgm0J1PM/nioxbOUdnM8HY=" crossorigin="anonymous">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="pb-2">
|
||||
<div
|
||||
class="flex h-16 items-center gap-8 px-4 sm:px-6 lg:px-8"
|
||||
>
|
||||
<a class="block text-teal-600" href="/">
|
||||
<span class="sr-only">Home</span>
|
||||
<img src="https://raw.githubusercontent.com/YunoHost/doc/master/images/logo_roundcorner.png" style="height: 3em;" />
|
||||
</a>
|
||||
|
||||
<div class="flex flex-1 items-center justify-end md:justify-between">
|
||||
<nav aria-label="Global" class="hidden md:block">
|
||||
<ul class="flex items-center gap-6 text-sm">
|
||||
<li>
|
||||
<a class="text-gray-800 font-bold transition hover:text-gray-500/75" href="{{ url_for('browse_catalog') }}">
|
||||
Catalog
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="text-gray-800 font-bold transition hover:text-gray-500/75" href="{{ url_for('browse_wishlist') }}">
|
||||
Wishlist
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="sm:flex sm:gap-4">
|
||||
<a
|
||||
class="hidden rounded-md bg-gray-100 px-5 py-2.5 text-sm font-medium text-teal-600 transition hover:text-teal-600/75 sm:block"
|
||||
href="https://yunohost.org/docs/"
|
||||
>
|
||||
<i class="fa fa-external-link fa-fw" aria-hidden="true"></i>
|
||||
YunoHost documentation
|
||||
</a>
|
||||
{% if not user %}
|
||||
<a
|
||||
class="block rounded-md bg-teal-600 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-teal-700"
|
||||
href="{{ url_for('login_using_discourse') }}"
|
||||
>
|
||||
Login using YunoHost's forum
|
||||
</a>
|
||||
{% else %}
|
||||
<button
|
||||
type="button"
|
||||
class="group flex shrink-0 items-center rounded-lg transition"
|
||||
>
|
||||
<span class="sr-only">Menu</span>
|
||||
<img
|
||||
alt="Man"
|
||||
src="{{ user['avatar_url'] }}"
|
||||
class="h-10 w-10 rounded-full object-cover"
|
||||
/>
|
||||
<p class="ms-2 hidden text-left text-xs sm:block">
|
||||
<strong class="block font-medium">{{ user['username'] }}</strong>
|
||||
</p>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="ms-4 hidden h-5 w-5 text-gray-500 transition group-hover:text-gray-700 sm:block"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<!--
|
||||
<a
|
||||
class="block rounded-md bg-teal-600 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-teal-700"
|
||||
href="{{ url_for('logout') }}"
|
||||
>
|
||||
Logout
|
||||
</a>
|
||||
-->
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="block rounded bg-gray-100 p-2.5 text-gray-600 transition hover:text-gray-600/75 md:hidden"
|
||||
>
|
||||
<span class="sr-only">Toggle menu</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% block main %}
|
||||
{% endblock %}
|
||||
|
||||
<footer class="text-center"><hr/>TODO : add a proper footer</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
113
store/templates/catalog.html
Normal file
113
store/templates/catalog.html
Normal file
|
@ -0,0 +1,113 @@
|
|||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
{% set locale = 'en' %}
|
||||
<div class="mt-5 text-center">
|
||||
<h2 class="text-2xl font-bold text-gray-900">
|
||||
Application Catalog
|
||||
</h2>
|
||||
</div>
|
||||
<div class="max-w-screen-md mx-auto mt-3 mb-3">
|
||||
<div class="flex flex-row">
|
||||
<div class="px-2 inline-block relative basis-2/3 text-gray-700">
|
||||
<label for="search" class="sr-only"> Search </label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
id="search"
|
||||
placeholder="Search for..."
|
||||
class="w-full rounded-md border-gray-200 shadow-sm sm:text-sm py-2.5 pe-10"
|
||||
/>
|
||||
|
||||
<span class="absolute inset-y-0 end-0 grid w-10 place-content-center pr-4">
|
||||
<i class="fa fa-search" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
<select
|
||||
name="selectcategory"
|
||||
id="selectcategory"
|
||||
class="w-full rounded-md border-gray-200 shadow-sm sm:test-sm px-2 basis-1/3 "
|
||||
>
|
||||
<option value="">All apps</option>
|
||||
{% for id, category in catalog['categories'].items() %}
|
||||
{{ category['title'][locale] }}
|
||||
<option value="{{ id }}">{{ category['title'][locale] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class=" mx-auto pt-2 text-center">
|
||||
<div class="inline-block px-2">
|
||||
<label for="sortbynew" class="inline-block relative h-4 w-7 cursor-pointer">
|
||||
<input type="checkbox" id="sortbynew" class="peer sr-only" />
|
||||
|
||||
<span class="absolute inset-0 rounded-full bg-gray-300 transition peer-checked:bg-green-500">
|
||||
</span>
|
||||
|
||||
<span class="absolute inset-y-0 start-0 m-1 h-2 w-2 rounded-full bg-white transition-all peer-checked:start-3">
|
||||
</span>
|
||||
</label>
|
||||
Sort by newest
|
||||
</div>
|
||||
<div class="inline-block px-2">
|
||||
<label for="onlyfav" class="inline-block relative h-4 w-7 cursor-pointer">
|
||||
<input type="checkbox" id="onlyfav" class="peer sr-only" />
|
||||
|
||||
<span class="absolute inset-0 rounded-full bg-gray-300 transition peer-checked:bg-green-500">
|
||||
</span>
|
||||
|
||||
<span class="absolute inset-y-0 start-0 m-1 h-2 w-2 rounded-full bg-white transition-all peer-checked:start-3">
|
||||
</span>
|
||||
</label>
|
||||
Show only your bookmarks
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 max-w-screen-lg mx-auto pt-10">
|
||||
{% for app, infos in catalog['apps'].items() %}
|
||||
<div>
|
||||
<a
|
||||
href="{{ url_for('app_info', app_id=app) }}"
|
||||
class="relative block overflow-hidden rounded-lg p-2 hover:bg-gray-200 "
|
||||
>
|
||||
<div class="sm:flex sm:justify-between sm:gap-4">
|
||||
<div class="sm:shrink-0">
|
||||
<img
|
||||
src="https://app.yunohost.org/default/v3/logos/{{ infos['logo_hash'] }}.png"
|
||||
class="h-16 w-16 rounded-lg object-cover shadow-sm mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<span class="flex">
|
||||
<h3 class="grow text-md font-bold text-gray-900">
|
||||
{{ infos['manifest']['name'] }}
|
||||
</h3>
|
||||
<span class="pt-1 pr-2 text-xs">
|
||||
{% if infos['level'] == 0 %}
|
||||
<i class="fa fa-exclamation-circle text-red-500 opacity-50" aria-hidden="true"></i>
|
||||
{% elif infos['level']|int <= 4 %}
|
||||
<i class="fa fa-exclamation-triangle text-orange-500 opacity-50" aria-hidden="true"></i>
|
||||
{% elif infos['level'] == 8 %}
|
||||
<i class="fa fa-star text-yellow-500 opacity-50" aria-hidden="true"></i>
|
||||
{% endif %}
|
||||
<span class="text-purple-500">
|
||||
123 <i class="fa fa-bookmark opacity-50" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<p class="max-w-[40ch] text-xs text-gray-500">
|
||||
{{ infos['manifest']['description']['en'] }}
|
||||
</p>
|
||||
{% if infos['category'] %}
|
||||
<span class="rounded-full bg-{{ catalog['categories'][infos['category']]['color'] }}-500 px-2.5 py-0.5 text-[10px] {% if catalog['categories'][infos['category']]['color'] != 'white' %} text-white {% else %} border border-gray-400 {% endif %} ">
|
||||
{{ catalog['categories'][infos['category']]['title'][locale].lower() }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
40
store/templates/index.html
Normal file
40
store/templates/index.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
{% set locale = 'en' %}
|
||||
|
||||
<div class="mx-auto w-full text-center p-8">
|
||||
<img src="https://raw.githubusercontent.com/YunoHost/doc/master/images/ynh_logo_black.svg" class="w-32 mx-auto" />
|
||||
<h2 class="text-2xl font-bold text-gray-900">
|
||||
Application Store
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 max-w-screen-lg mx-auto pt-5">
|
||||
<div class="text-center border rounded-lg h-32">
|
||||
<a
|
||||
href="{{ url_for('browse_catalog') }}"
|
||||
class="h-full relative block overflow-hidden hover:bg-gray-200 pt-12"
|
||||
>
|
||||
<h3 class="text-md font-bold text-gray-900">
|
||||
Browse all applications
|
||||
</h3>
|
||||
</a>
|
||||
</div>
|
||||
{% for id, category in catalog['categories'].items() %}
|
||||
<div class="text-center border rounded-lg h-32">
|
||||
<a
|
||||
href="{{ url_for('browse_catalog', category_filter=id) }}"
|
||||
class="h-full relative block overflow-hidden hover:bg-gray-200 pt-10"
|
||||
>
|
||||
<h3 class="text-md font-bold text-gray-900">
|
||||
<i class="fa fa-{{ category['icon'] }}" aria-hidden="true"></i>
|
||||
{{ category['title'][locale] }}
|
||||
</h3>
|
||||
<p class="mx-auto max-w-[40ch] text-xs text-gray-500">
|
||||
{{ category['description'][locale] }}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
103
store/templates/wishlist.html
Normal file
103
store/templates/wishlist.html
Normal file
|
@ -0,0 +1,103 @@
|
|||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<div class="mt-5 text-center">
|
||||
<h2 class="text-2xl font-bold text-gray-900">
|
||||
Application Wishlist
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="max-w-screen-md mx-auto mt-3 mb-3">
|
||||
<div class="flex flex-row">
|
||||
<div class="px-2 inline-block relative basis-2/3 text-gray-700">
|
||||
<label for="search" class="sr-only"> Search </label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
id="search"
|
||||
placeholder="Search for..."
|
||||
class="w-full rounded-md border-gray-200 shadow-sm sm:text-sm py-2.5 pe-10"
|
||||
/>
|
||||
|
||||
<span class="absolute inset-y-0 end-0 grid w-10 place-content-center pr-4">
|
||||
<i class="fa fa-search" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<a
|
||||
|
||||
class="inline-block rounded border text-blue-600 border-blue-500 px-4 pt-3 text-sm font-medium hover:bg-blue-500 hover:text-white"
|
||||
href="#"
|
||||
>
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
|
||||
Add an app to the wishlist
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="overflow-x-auto max-w-screen-lg mx-auto pt-5">
|
||||
<table class="min-w-full divide-y-2 divide-gray-200 bg-white text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap px-4 py-2 font-medium text-gray-900">
|
||||
Name
|
||||
</th>
|
||||
<th class="whitespace-nowrap px-4 py-2 font-medium text-gray-900">
|
||||
Description
|
||||
</th>
|
||||
<th class="px-1 py-2"></th>
|
||||
<th class="px-1 py-2"></th>
|
||||
<th class="px-1 py-2"></th>
|
||||
<th class="px-1 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
{% for infos in wishlist %}
|
||||
<tr>
|
||||
<td class="px-4 py-2 font-bold text-gray-900 max-w-[10em]">
|
||||
{{ infos['name'] }}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-gray-700 max-w-md">{{ infos['description'] }}</td>
|
||||
<td class="px-1 py-2">
|
||||
{% if infos['website'] %}
|
||||
<a
|
||||
href="{{ infos['website'] }}"
|
||||
class="inline-block"
|
||||
>
|
||||
<i class="fa fa-globe rounded border px-3 py-2 hover:bg-gray-100" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-1 py-2">
|
||||
{% if infos['upstream'] %}
|
||||
<a
|
||||
href="{{ infos['upstream'] }}"
|
||||
class="inline-block"
|
||||
>
|
||||
<i class="fa fa-code rounded border px-3 py-2 hover:bg-gray-100" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-1 py-2">
|
||||
<a
|
||||
href="#"
|
||||
class="inline-block rounded border text-blue-600 border-blue-500 px-4 py-2 text-xs font-medium hover:bg-blue-500 hover:text-white"
|
||||
>
|
||||
<i class="fa fa-bookmark fa-fw" aria-hidden="true"></i>
|
||||
Vote
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-1 py-2">
|
||||
<span class="text-blue-500">
|
||||
123 <i class="fa fa-bookmark" aria-hidden="true"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Add table
Reference in a new issue