Connect OSM account
All checks were successful
CI / Lint (pull_request) Successful in 30s
CI / Test (pull_request) Successful in 49s
Release Drafter / Update release notes draft (pull_request) Successful in 4s

This commit is contained in:
2026-04-01 18:35:01 +04:00
parent 2dfd411837
commit e7dfed204e
12 changed files with 482 additions and 8 deletions

View File

@@ -1,9 +1,13 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { service } from '@ember/service';
import Icon from '#components/icon';
import { on } from '@ember/modifier';
export default class UserMenuComponent extends Component {
@service storage;
@service osmAuth;
@action
connectRS() {
this.args.onClose();
@@ -15,6 +19,17 @@ export default class UserMenuComponent extends Component {
this.args.storage.disconnect();
}
@action
connectOsm() {
this.args.onClose();
this.osmAuth.login();
}
@action
disconnectOsm() {
this.osmAuth.logout();
}
<template>
<div class="user-menu-popover">
<ul class="account-list">
@@ -40,22 +55,39 @@ export default class UserMenuComponent extends Component {
</div>
<div class="account-status">
{{#if @storage.connected}}
<strong>{{@storage.userAddress}}</strong>
{{@storage.userAddress}}
{{else}}
Not connected
{{/if}}
</div>
</li>
<li class="account-item disabled">
<li class="account-item">
<div class="account-header">
<div class="account-info">
<Icon @name="map" @size={{18}} />
<span>OpenStreetMap</span>
</div>
{{#if this.osmAuth.isConnected}}
<button
class="btn-text text-danger"
type="button"
{{on "click" this.disconnectOsm}}
>Disconnect</button>
{{else}}
<button
class="btn-text text-primary"
type="button"
{{on "click" this.connectOsm}}
>Connect</button>
{{/if}}
</div>
<div class="account-status">
Coming soon
{{#if this.osmAuth.isConnected}}
{{this.osmAuth.userDisplayName}}
{{else}}
Not connected
{{/if}}
</div>
</li>

View File

@@ -10,4 +10,7 @@ Router.map(function () {
this.route('place', { path: '/place/:place_id' });
this.route('place.new', { path: '/place/new' });
this.route('search');
this.route('oauth', function () {
this.route('osm-callback', { path: '/osm/callback' });
});
});

View File

@@ -0,0 +1,17 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
export default class OauthOsmCallbackRoute extends Route {
@service osmAuth;
@service router;
async model() {
try {
await this.osmAuth.handleCallback();
} catch (e) {
console.error('Failed to handle OSM OAuth callback', e);
} finally {
this.router.transitionTo('index');
}
}
}

103
app/services/osm-auth.js Normal file
View File

@@ -0,0 +1,103 @@
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { OAuth2AuthCodePkceClient } from 'oauth2-pkce';
class MarcoOsmAuthStorage {
saveState(serializedState) {
localStorage.setItem('marco:osm_auth_state', serializedState);
}
loadState() {
return localStorage.getItem('marco:osm_auth_state');
}
}
export default class OsmAuthService extends Service {
@tracked isConnected = false;
@tracked userDisplayName = null;
oauthClient;
constructor() {
super(...arguments);
const clientId =
import.meta.env.VITE_OSM_CLIENT_ID || 'YOUR_CLIENT_ID_HERE';
const oauthUrl =
import.meta.env.VITE_OSM_OAUTH_URL || 'https://www.openstreetmap.org';
const redirectUrl =
import.meta.env.VITE_OSM_REDIRECT_URI ||
`${window.location.origin}/oauth/osm/callback`;
this.oauthClient = new OAuth2AuthCodePkceClient(
{
scopes: ['read_prefs', 'write_api'],
authorizationUrl: `${oauthUrl}/oauth2/authorize`,
tokenUrl: `${oauthUrl}/oauth2/token`,
clientId: clientId,
redirectUrl: redirectUrl,
storeRefreshToken: true,
},
new MarcoOsmAuthStorage()
);
this.restoreSession();
}
async restoreSession() {
const isAuthorized = await this.oauthClient.isAuthorized();
if (isAuthorized) {
this.isConnected = true;
const storedName = localStorage.getItem('marco:osm_user_display_name');
if (storedName) {
this.userDisplayName = storedName;
} else {
await this.fetchUserInfo();
}
}
}
async login() {
await this.oauthClient.requestAuthorizationCode();
}
async handleCallback() {
await this.oauthClient.receiveCode();
await this.oauthClient.getTokens();
this.isConnected = true;
await this.fetchUserInfo();
}
async logout() {
await this.oauthClient.reset();
this.isConnected = false;
this.userDisplayName = null;
localStorage.removeItem('marco:osm_user_display_name');
}
async fetchUserInfo() {
try {
const tokens = await this.oauthClient.getTokens();
// eslint-disable-next-line warp-drive/no-external-request-patterns
const response = await fetch(
'https://api.openstreetmap.org/api/0.6/user/details.json',
{
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
}
);
if (response.ok) {
const data = await response.json();
console.debug('OSM data:', data);
const displayName = data.user.display_name;
this.userDisplayName = displayName;
localStorage.setItem('marco:osm_user_display_name', displayName);
} else {
console.error('Failed to fetch OSM user info', response.status);
}
} catch (e) {
console.error('Error fetching OSM user info', e);
}
}
}

View File

@@ -246,6 +246,7 @@ body {
.account-status {
font-size: 0.85rem;
font-weight: bold;
color: #898989;
margin-top: 0.35rem;
margin-left: calc(18px + 0.75rem);